Si vas a escalar Odoo en serio (más usuarios concurrentes, más jobs, más integraciones), el cuello de botella casi siempre termina siendo las conexiones a PostgreSQL y cómo las gestionas. La combinación Odoo + PgBouncer + PostgreSQL es el estándar de facto… pero solo si la armas bien (especialmente por el tema LISTEN/NOTIFY del bus y el /websocket/).
Aquí tienes una arquitectura clara, robusta y fácil de operar.
Objetivo de esta arquitectura
Reducir conexiones reales a PostgreSQL sin matar concurrencia.
Evitar “Connection Pool Is Full” y picos de latencia.
Mantener compatibilidad con notificaciones en tiempo real (bus) y WebSocket.
Facilitar HA / escalado horizontal del lado de Odoo.
Diagrama recomendado (simple y escalable)
Internet
|
[ Nginx/HAProxy ]
TLS + gzip + limits
/websocket/ | /
| | /
v v v
[ Odoo gevent ] [ Odoo workers ]
:8072 :8069
\ /
\ /
v v
[ PgBouncer (transaction) ]
:6432
|
v
[ PostgreSQL ]
:5432
Idea clave:
Odoo “normal” (ORM/HTTP) va a PgBouncer transaction pooling.
El canal de WebSocket va a gevent_port y se enruta separado en el proxy.
Por qué PgBouncer en transaction pooling
Odoo hace muchísimas transacciones cortas (lecturas, writes pequeños). Con pool_mode = transaction:
Reutilizas conexiones “servidor” (PgBouncer→Postgres) de forma eficiente.
Aceptas muchos clientes (workers) sin inflar max_connections en Postgres.
Mejoras latencia bajo carga.
Pero: transaction pooling NO sirve para cosas “de sesión” (y ahí entra el bus).
El “gotcha” real: LISTEN/NOTIFY (bus) y PgBouncer
Odoo usa notificaciones para eventos en tiempo real (bus). En PostgreSQL, LISTEN es por sesión. En PgBouncer transaction tu “sesión” cambia: hoy escuchas, mañana te mueven a otra conexión. Resultado: el bus se vuelve inestable.
Soluciones correctas (elige 1)
Opción A (recomendada): Odoo normal → PgBouncer (transaction) y bus/listen directo a Postgres
Más simple, menos piezas.
Requiere separar esa conexión (por config/módulo/ajuste según tu versión y stack).
Opción B: 2 PgBouncer
pgbouncer_tx (transaction) para Odoo normal
pgbouncer_sess (session) solo para LISTEN
Más complejo, pero todo pasa por PgBouncer.
Configuración base por componente
1) Nginx (reverse proxy)
Separar /websocket/ es obligatorio si usas gevent/websocket:
upstream odoo_http {
server 127.0.0.1:8069;
}
upstream odoo_ws {
server 127.0.0.1:8072;
}
server {
listen 443 ssl http2;
server_name tu-dominio.com;
# TLS config aquí...
# WebSocket
location /websocket/ {
proxy_pass http://odoo_ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Resto de Odoo
location / {
proxy_pass http://odoo_http;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 600s;
proxy_connect_timeout 30s;
proxy_send_timeout 600s;
}
}
En Odoo: proxy_mode = True.
2) Odoo (producción)
Puntos de oro:
workers > 0 (multi-process)
proxy_mode = True
DB normal apuntando a PgBouncer
WebSocket separado (gevent port)
Ejemplo:
[options] proxy_mode = True workers = 8 max_cron_threads = 2 http_port = 8069 gevent_port = 8072 db_host = 127.0.0.1 db_port = 6432 db_user = odoo db_password = ******** db_maxconn = 64
db_maxconn es por proceso: si lo subes sin pensar y tienes muchos workers, puedes presionar PgBouncer y Postgres.
3) PgBouncer (transaction pooling)
Ejemplo práctico:
[databases] odoodb = host=127.0.0.1 port=5432 dbname=odoodb [pgbouncer] listen_addr = 0.0.0.0 listen_port = 6432 auth_type = scram-sha-256 auth_file = /etc/pgbouncer/userlist.txt pool_mode = transaction max_client_conn = 2000 default_pool_size = 50 reserve_pool_size = 20 reserve_pool_timeout = 5 server_idle_timeout = 60 server_reset_query = DISCARD ALL ignore_startup_parameters = extra_float_digits
Notas rápidas:
default_pool_size = conexiones reales “servidor” por db/user.
reserve_pool_size ayuda en picos cortos.
DISCARD ALL limpia sesión cuando cambia cliente (recomendado con transaction pooling).
4) PostgreSQL
Reglas prácticas:
max_connections debe cubrir:
conexiones “servidor” que PgBouncer abrirá (pool_size + reserve)
conexiones directas (monitoring, mantenimiento, bus directo si aplica)
En pg_hba.conf, no te olvides de permitir el host de PgBouncer y Odoo según corresponda, y mantenerlo cerrado a Internet.
Cómo dimensionar (sin magia)
1) Odoo workers
Una guía útil:
workers ≈ (CPU cores * 2) + 1 (ajústalo por carga real y RAM)
2) PgBouncer default_pool_size
Si Postgres está en el mismo host y tu DB es rápida: 20–80 suele funcionar.
Si hay mucha latencia o queries pesadas: pool más pequeño y optimiza queries/índices.
3) Postgres max_connections
Piensa así:
max_connections >= (default_pool_size + reserve_pool_size) * (db/user pools) + margen
Si solo tienes 1 db y 1 user: es simple. Si tienes varios, suma.
Dos arquitecturas finales “listas para producción”
Arquitectura 1 (recomendada)
1 PgBouncer (transaction)
Bus/listen directo a Postgres (solo esa conexión)
Proxy separando websocket a gevent
✅ Mejor relación simplicidad/estabilidad.
Arquitectura 2 (enterprise/strict)
PgBouncer TX (transaction) para Odoo normal
PgBouncer SESS (session) para bus/listen
Postgres detrás, sin bypass
✅ Todo pasa por proxy/poolers, pero aumenta complejidad.
Observabilidad: lo mínimo que debes monitorear
En PgBouncer
SHOW POOLS; (cola, activos, waiting)
SHOW STATS; (latencia, tx/s)
En PostgreSQL
pg_stat_activity (states, waits)
top queries (pg_stat_statements si lo usas)
En Odoo
latencia de endpoints
colas de cron
errores de pool/timeout
Checklist de “arquitectura bien hecha”
Nginx enruta /websocket/ al gevent_port
proxy_mode = True
Odoo normal → PgBouncer pool_mode=transaction
Bus/listen NO depende de transaction pooling (bypass o session pooling)
default_pool_size y max_connections calculados (no al azar)
límites y timeouts razonables en proxy y pooler
autenticación segura (SCRAM), y red cerrada entre componentes
Cierre
La arquitectura Odoo + PgBouncer + PostgreSQL funciona excelente cuando:
separas bien HTTP vs WebSocket,
usas PgBouncer transaction para el ORM,
tratas el bus (LISTEN) como un caso especial,
dimensionas pools con números, no con fe.
Si quieres, te dejo una propuesta exacta de valores (workers, db_maxconn, default_pool_size, max_connections) si me dices:
CPU/RAM del servidor
si Postgres está en el mismo host o remoto
concurrencia objetivo (usuarios simultáneos + crons pesados + integraciones)