La diferencia entre “se puso lento” y “fue un incidente” es cuándo te enteras.
Si esperas a que el usuario se queje, ya vas tarde. La buena noticia: en un stack Odoo → PgBouncer → PostgreSQL, la saturación deja huellas claras minutos antes.
Este post te da un sistema simple: 4 señales tempranas + umbrales prácticos + qué hacer según el patrón.
1) Señal temprana #1: aparece cola en PgBouncer
Qué mirar
En la consola admin de PgBouncer:
SHOW POOLS;
Los dos indicadores que te adelantan el problema son:
cl_waiting > 0 sostenido
maxwait subiendo (el cliente más antiguo lleva esperando)
Interpretación: tu app está intentando iniciar transacciones, pero PgBouncer no consigue asignarles una conexión “server” lo suficientemente rápido. Eso es “saturación” en tiempo real, incluso si todavía no hay errores visibles.
Umbral útil
Warning: cl_waiting > 0 durante 60–120s
Critical: maxwait > 1s sostenido (ya afecta UX), > 5s es incidente
Acción inmediata
Si hay cola: no adivines. Sigue al paso 4 (clasificar causa).
2) Señal temprana #2: crece el porcentaje de transacciones largas
Qué mirar (PostgreSQL)
Consulta rápida para ver sesiones y transacciones largas:
SELECT pid, usename, 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;
Señales de alerta
xact_start “viejo” (transacciones > 60–120s en horas pico)
muchas sesiones con wait_event_type = Lock
Interpretación: aunque el CPU esté “ok”, las transacciones largas secuestran concurrencia (y con PgBouncer, secuestran conexiones server).
Umbral útil
Warning: 1–3 transacciones > 2 min en horario de carga
Critical: transacciones > 5–10 min (casi siempre bloqueo/cron monstruo)
3) Señal temprana #3: el throughput cae pero la demanda no (la “muerte lenta”)
Qué mirar
Requests/segundo (o jobs/segundo) vs latencia
PgBouncer SHOW STATS; (si lo estás recolectando)
Odoo: latencia p95/p99 por endpoint (login, listado, write, confirmaciones, reportes)
Patrón clásico previo al incidente
p95 sube lentamente
p99 se dispara primero
throughput no sube (o cae) aunque haya tráfico normal
Interpretación: ya no estás “escalando” con carga. Estás en contención.
Umbral útil
Warning: p99 > 2–3x tu baseline
Critical: errores por timeout o retries masivos
4) Señal temprana #4: los crons empiezan a solaparse (y nadie mira)
Esto es brutal en Odoo.
Qué mirar
duración de crons pesados
horario real de ejecución vs esperado
si se están superponiendo (sobre todo si tienes max_cron_threads > 1)
Patrón previo al incidente
cron A tarda más → cron B arranca igual → ambos compiten por locks y DB
PgBouncer empieza a hacer cola
usuarios notan lentitud “en oleadas”
Umbral útil
Warning: cron que pasa de X min a 2X min de forma repetida
Critical: backlog (crons no terminan antes de su próximo run)
5) La clasificación clave: 3 tipos de saturación (y qué hacer)
Cuando detectas saturación temprana, clasifícala en una de estas 3.
Esto evita el error típico de “subir pool_size y listo”.
Tipo A — Saturación por pool (config/concurrencia)
Síntomas
cl_waiting sube
sv_idle ~ 0
Postgres NO está al 100%
no hay grandes locks, solo “mucho movimiento”
Acciones
sube default_pool_size con cuidado
agrega reserve_pool_size para picos
revisa si max_client_conn o max_db_connections te están limitando
Tipo B — Saturación por locks / transacciones largas
Síntomas
cl_waiting sube
maxwait sube sostenido
en Postgres ves wait_event_type = Lock o xact_start muy viejo
CPU no necesariamente alto (es contención)
Acciones
identifica la transacción larga (job/cron/acción de usuario)
corta duración: batching, commits por lote, evitar I/O externo en la transacción
añade lock_timeout, statement_timeout e idle_in_transaction_session_timeout (según política)
Tipo C — Saturación de recursos (CPU/RAM/I/O)
Síntomas
CPU al 100% o I/O wait alto
latencia sube en todos lados
PgBouncer puede mostrar cola, pero el problema raíz es el host/DB
Acciones
optimizar queries/índices
bajar concurrencia (workers/crons) para reducir contención
mejorar disco/IOPS
revisar bloat/autovacuum si el rendimiento cae con el tiempo
6) Un set mínimo de alertas “anti-incendio”
Si solo pudieras crear 6 alertas, serían estas:
PgBouncer
cl_waiting > 0 por 2 min
maxwait > 1s por 2 min
PostgreSQL
transacciones > 2 min (conteo > N)
sesiones esperando locks > N
Odoo / app
p99 latencia > 2–3x baseline
tasa de errores (timeouts/5xx) > baseline
7) El truco que más te da tiempo: “alerta por tendencia”, no por caída
Muchos monitorean “CPU > 90%”. Eso llega tarde.
Lo que te compra tiempo es alertar por cambio de comportamiento:
p99 sube 30–50% respecto al baseline
maxwait pasa de 0 a 0.5s y sigue subiendo
crons empiezan a durar 2x
Eso ocurre antes de que el usuario note el dolor.
Cierre
Si quieres detectar saturación antes que el usuario:
mide cola en PgBouncer,
mide transacciones largas y locks en Postgres,
mide p95/p99 en Odoo,
y vigila crons como si fueran usuarios (porque lo son, pero más peligrosos).