Hay dos cosas que pueden hacer que un Odoo aparentemente “bien dimensionado” se vuelva lento de golpe:
Cron jobs corriendo en paralelo (y tocando muchas filas)
Transacciones largas (long transactions) que mantienen la DB “ocupada” demasiado tiempo
Cuando sumas PgBouncer (especialmente en pool_mode=transaction), el efecto se amplifica: cada transacción larga “secuestra” una conexión server del pool, y lo que ves en la app no es “Postgres lento”, sino cola.
1) Qué ocurre en la práctica (la cadena causal)
A) Crons: más paralelismo = más presión a DB
Odoo permite correr crons concurrentes con max_cron_threads (por defecto suele ser 2).
Eso está buenísimo… hasta que tus crons:
procesan miles de registros,
actualizan tablas calientes,
disparan recomputes,
o hacen integraciones lentas (APIs + escritura en DB).
Resultado: sube la cantidad de transacciones simultáneas.
B) Long transactions: el “bloqueo silencioso”
Una transacción larga no solo “tarda”: también puede:
retener locks (y bloquear a otras),
aumentar contención,
y, con PgBouncer en transaction pooling, retener una conexión server hasta que termine.
PgBouncer lo deja claro: en transaction pooling, una conexión server se libera cuando la transacción termina.
2) Síntomas típicos (lo que ves en producción)
En PgBouncer
cl_waiting empieza a subir (clientes esperando conexión server)
GitLab runbooks lo resume: cl_waiting indica que clientes quieren ejecutar una transacción pero PgBouncer no pudo asignarles una conexión server de inmediato, y una causa frecuente son transacciones largas “hogging” conexiones.
En Odoo
picos de latencia (formularios que tardan “en oleadas”)
timeouts intermitentes
crons que se pisan y se vuelven eternos (efecto bola de nieve)
En PostgreSQL
sesiones con state activo mucho tiempo
locks que se acumulan (una transacción larga puede mantener locks y bloquear escrituras)
3) Por qué los crons “exageran” el problema
Porque los crons suelen ser trabajos masivos:
recorren lotes grandes,
hacen write() en bucle,
recalculan campos,
crean/validan documentos,
o limpian datos.
Y como max_cron_threads habilita concurrencia, de repente tienes N transacciones masivas al mismo tiempo.
Además, Odoo incluye servidor de cron dentro del stack (junto con HTTP y live-chat), y en producción normalmente se usa multi-processing.
4) Checklist de diagnóstico rápido (copiar/pegar)
A) PgBouncer: ¿hay cola?
En la consola admin:
SHOW POOLS; SHOW STATS;
Qué te interesa:
cl_waiting subiendo → falta conexión server disponible o conexiones “secuestradas”.
B) PostgreSQL: ¿quién está “abrazando” la DB?
SELECT pid, usename, application_name, state, xact_start, query_start, wait_event_type, wait_event, query FROM pg_stat_activity WHERE datname = current_database() ORDER BY xact_start NULLS LAST;
Fíjate en:
xact_start muy antiguo (transacción larga)
wait_event_type relacionado a locks
C) Locks: ¿hay bloqueos encadenados?
SELECT locktype, mode, granted, count(*) FROM pg_locks GROUP BY 1,2,3 ORDER BY 4 DESC;
5) Cómo mitigarlo (sin “bajar todo”)
1) Domar la concurrencia de cron
Empieza con max_cron_threads = 1–2 y sube solo con evidencia.
Si tienes un cron “monstruo”, no lo corras al mismo tiempo que otros pesados (separa horarios).
2) Evitar transacciones largas en crons (el fix #1)
En vez de “procesar todo”, procesa en lotes:
busca IDs en chunks (ej. 500–2000)
procesa
confirma/invalida lo necesario
y vuelve por el siguiente lote
Beneficio: cada lote es una transacción más corta ⇒ liberas conexiones en PgBouncer antes ⇒ menos cl_waiting.
3) Identificar “long transactions” por diseño
Causas comunes:
crons que hacen demasiados write() en loop
integraciones externas dentro de la misma transacción
reportes/acciones masivas que el usuario ejecuta y quedan “colgadas”
Regla de oro: I/O externo (APIs) fuera de la transacción siempre que sea posible.
4) Protecciones en Postgres
statement_timeout: evita queries infinitas
lock_timeout: evita quedarte esperando locks eternamente (mejor fallar rápido y reintentar)
idle_in_transaction_session_timeout: mata sesiones olvidadas “idle in transaction”
5) Protecciones en PgBouncer (si hay picos)
usa reserve_pool_size para burst corto
monitorea cl_waiting (si crece sostenido, no es “pico”: es diseño o query)
6) El consejo que más ahorra incidentes
Si tu sistema se vuelve lento a ciertas horas y coincide con crons:
No subas default_pool_size primero.
Primero confirma si hay transacciones largas reteniendo conexiones.
Luego reduce duración (batching) o reduce concurrencia (max_cron_threads) antes de “darle más conexiones” a Postgres.