No todas las empresas necesitan “arquitectura enterprise”, pero todas necesitan estabilidad. La forma más práctica de acertar en Odoo es elegir una configuración base según tamaño (y luego ajustar con métricas).
En este post te dejo tres perfiles (pequeña, mediana, grande) con valores recomendados para:
Odoo: workers, max_cron_threads, db_maxconn
PgBouncer: pool_mode, default_pool_size, reserve_pool_size, max_client_conn
PostgreSQL: max_connections (orientativo)
Observabilidad mínima
Supuestos:
Odoo detrás de Nginx/HAProxy (TLS)
PgBouncer en pool_mode=transaction para el tráfico ORM (lo típico con Odoo)
1 DB principal por instancia (si tienes multi-tenant, ver nota al final)
0) Regla rápida para leer esta guía
Workers te dan concurrencia web (capacidad de atender requests).
PgBouncer convierte esa concurrencia en un número controlado de conexiones reales a Postgres.
Cron jobs pueden destruir tu p99 si corren demasiado en paralelo.
Si dudas: empieza conservador, mide SHOW POOLS; y ajusta.
Perfil 1: Empresa pequeña (PyME / 10–50 usuarios)
Hardware típico
Odoo/App: 2–4 vCPU, 8–16 GB RAM
Postgres: mismo host o separado (ideal 2–4 vCPU, 8–16 GB RAM)
Odoo (odoo.conf)
workers = 5–9
max_cron_threads = 1
db_maxconn = 32
Por qué así:
El enemigo #1 aquí es swap y crons que se pisan. Mejor menos concurrencia, pero estable.
PgBouncer (pgbouncer.ini)
pool_mode = transaction
default_pool_size = 20–40
reserve_pool_size = 5–10
reserve_pool_timeout = 5
max_client_conn = 500–1000
PostgreSQL (orientativo)
max_connections = 150–250 (no lo inflas “por si acaso”; PgBouncer manda)
“Señales” a vigilar
cl_waiting en PgBouncer (si aparece sostenido, primero mira crons/locks)
duración del cron más pesado
Perfil 2: Empresa mediana (50–300 usuarios)
Hardware típico
Odoo/App: 8 vCPU, 32 GB RAM
Postgres: 8 vCPU, 32–64 GB RAM (ideal separado)
Odoo
workers = 13–17
max_cron_threads = 2
db_maxconn = 64
Por qué así:
Acá ya importa el throughput. Dos crons en paralelo está bien, pero si tienes crons masivos, tendrás que hacer batching.
PgBouncer
pool_mode = transaction
default_pool_size = 60–100
reserve_pool_size = 15–30
reserve_pool_timeout = 5
max_client_conn = 1500–3000
Tip: si default_pool_size sube y Postgres empeora, no sigas subiendo: estás en contención/locks.
PostgreSQL (orientativo)
max_connections = 250–400
“Señales” a vigilar
maxwait en PgBouncer
transacciones > 2 min en Postgres (predicen cola)
p99 en operaciones críticas (validaciones, cierres, inventario)
Perfil 3: Empresa grande (300–1500+ usuarios / operación intensiva)
Hardware típico (mínimo razonable)
Odoo/App: 16–32 vCPU, 64–128 GB RAM (a menudo 2+ nodos Odoo)
Postgres: 16–32 vCPU, 128 GB+ RAM, discos rápidos (IOPS importan)
Odoo
workers = 25–45 (según cores y RAM)
max_cron_threads = 2–4 (solo si crons están bien diseñados)
db_maxconn = 64 (muchas veces mejor no subirlo más; escala PgBouncer/DB)
Por qué así:
En grande, el enemigo es la contención (locks) y las transacciones largas. Más threads no arreglan diseño.
PgBouncer
pool_mode = transaction
default_pool_size = 120–200 (si Postgres lo soporta)
reserve_pool_size = 30–60
reserve_pool_timeout = 5
max_client_conn = 5000–10000
Extra importante: define límites:
max_db_connections para no dejar que una DB “se coma” todo
y alertas sobre cl_waiting antes de incidentes
PostgreSQL (orientativo)
max_connections = 400–800 (con PgBouncer bien hecho, no necesitas miles)
“Señales” a vigilar
locks y bloqueos recurrentes (deadlocks/serialization)
crecimiento de p99 cuando corren crons o importaciones
autovacuum (si se atrasa, el rendimiento se degrada con el tiempo)
Nota: si tienes multi-tenant (varias DBs en el mismo PgBouncer)
Recuerda que PgBouncer crea pools por (db, user). Eso significa que:
si tienes N bases, el “presupuesto” de conexiones reales se reparte entre N pools
conviene usar max_db_connections y calcular default_pool_size con ese reparto
Config “mínima común” para cualquier tamaño (la base que no falla)
Odoo
proxy_mode = True
/websocket/ ruteado al gevent_port (si usas livechat/tiempo real)
list_db = False
logs con rotación
PgBouncer
pool_mode = transaction
logs útiles (log_pooler_errors=1)
SHOW POOLS; como métrica #1
PostgreSQL
timeouts razonables (lock_timeout, statement_timeout)
monitoreo de transacciones largas
Cierre: el mejor consejo para que esto funcione
No dimensionas “por intuición”, dimensionas por señales:
Si hay cl_waiting: o el pool es chico o hay transacciones largas/locks.
Si Postgres está al 100%: no subas pool, optimiza queries/índices o baja concurrencia.
Si los crons se pisan: reduce paralelismo o haz batching.