La “interacción real” entre pooling (por ejemplo PgBouncer) y el ORM de Odoo se entiende mejor si lo mirás como tres capas que se enciman:
Request / Job (HTTP o cron)
ORM (env/cr/cursor + transacción)
Conexión PostgreSQL (y el pooling delante)
Abajo te lo bajo a tierra con el flujo real y los puntos donde suele romperse o degradar.
1) Qué hace Odoo de verdad: cursor = transacción
En Odoo, el ORM trabaja sobre env, y env.cr es un cursor psycopg2 asociado a una conexión. En la práctica:
Cada request (o job cron) entra a un worker.
Odoo crea un cursor (cr) y empieza una transacción.
El ORM ejecuta SQL a través de ese cursor (con prefetch/cache a nivel ORM, pero siempre dentro de esa transacción).
Al final:
COMMIT si todo OK
ROLLBACK si hay excepción
Se cierra el cursor y la conexión vuelve al pool (pool de Odoo o pool externo).
Clave: Para Odoo, “unidad de trabajo” suele ser una transacción por request (o por job cron), salvo casos especiales (sub-transacciones con savepoints).
2) Odoo ya tiene pooling interno (aunque mucha gente lo subestima)
Odoo mantiene un pool de conexiones por proceso (cada worker). Eso significa:
Un worker no abre una conexión fija necesariamente: puede abrir varias hasta db_maxconn (parámetro de Odoo).
En producción, lo común es que cada worker use pocas conexiones, pero en picos puede crecer.
Si tenés workers = N, el peor caso de conexiones a Postgres puede acercarse a:
N * db_maxconn (+ conexiones auxiliares, cron, etc.)
Por eso PgBouncer es tan popular: te “aplana” ese pico hacia Postgres.
3) PgBouncer: por qué el modo de pooling cambia TODO
PgBouncer tiene 3 modos (los que importan acá):
A) pool_mode = session (lo más compatible)
Un “cliente” mantiene el mismo backend Postgres durante toda la sesión.
Odoo “cree” que su conexión es estable (y lo es).
Menos sorpresas con estado de sesión.
✅ Es el modo que suele “funcionar siempre” con Odoo.
B) pool_mode = transaction (tentador, pero con trampas)
PgBouncer asigna un backend Postgres solo durante la transacción.
Al hacer COMMIT/ROLLBACK, el backend vuelve al pool y el próximo request puede caer en otro backend.
Esto puede ir bien si y solo si tu app no depende de estado de sesión fuera de la transacción.
Dónde Odoo se puede complicar en transaction pooling:
Advisory locks de sesión (pg_advisory_lock, pg_try_advisory_lock): si Odoo toma locks que dependen de “la sesión”, y PgBouncer te cambia el backend, ese lock puede quedar en un backend distinto al esperado.
Cualquier cosa que dependa de “me quedo en el mismo backend” fuera de una única transacción.
Hay despliegues que lo hacen andar, pero es donde aparecen bugs “fantasma” (crons que se pisan, locks que no se respetan, comportamiento raro bajo carga).
C) pool_mode = statement
Casi nunca para Odoo. Demasiado agresivo.
4) “Interacción real” ORM ↔ pooling: el timeline que importa
Request HTTP típico (con workers)
Worker recibe request
Odoo toma una conexión (del pool interno)
Abre cr (cursor) → inicia transacción
ORM hace lecturas/escrituras (prefetch/cache no cambia el hecho de que estás dentro de esa transacción)
COMMIT/ROLLBACK
Devuelve conexión al pool
Con PgBouncer session pooling: ese “paso 2” termina en un backend Postgres estable.
Con PgBouncer transaction pooling: el backend Postgres solo está garantizado durante 3–5. En el 6, el backend se libera.
5) El punto más importante: “estado de sesión” vs “estado por transacción”
Para que PgBouncer en transaction sea seguro, tu app debe cumplir:
“Todo lo que necesito vive dentro de la transacción; no asumo continuidad de sesión”.
Odoo, en general, está bastante cerca de eso para la mayoría del tráfico HTTP (porque request=transacción), pero hay dos zonas donde puede doler:
Locks: si algo usa locks de sesión y espera estabilidad, transaction pooling los vuelve impredecibles.
Jobs/cron: tienden a hacer operaciones largas y coordinación entre procesos; si hay locks de sesión o “coordinación por conexión”, transaction pooling puede romper invariantes.
Por eso, la recomendación práctica suele ser:
Si querés cero sorpresas: PgBouncer session.
Si necesitás exprimir al máximo la reducción de conexiones: transaction, pero testeando específicamente crons/locks/concurrencia real.
6) Números útiles: cómo dimensionar “pooling real”
Supongamos:
workers = 12
db_maxconn = 8
Sin PgBouncer (peor caso): hasta ~96 conexiones a Postgres desde Odoo.
Con PgBouncer: podés limitar “hacia Postgres” con default_pool_size/max_db_connections (según tu setup), y aceptar muchas conexiones “cliente” desde Odoo a PgBouncer, mientras PgBouncer multiplexa.
La clave es que, aunque bajes conexiones a Postgres, no podés bajar “concurrencia efectiva” sin impactar latencia: si tenés 12 workers que necesitan DB y solo 10 backends, 2 van a esperar.
7) Recomendación práctica para Odoo (sin vueltas)
Producción “normal” (ERP + crons + módulos varios): PgBouncer en session.
Si vas por transaction, hacelo sabiendo que estás comprando complejidad:
probá a fondo crons con carga
probá escenarios de locks / contención
medí si realmente necesitabas transaction o si con session + buen sizing ya estabas
Si querés, te lo aterrizo a tu caso concreto: decime (aunque sea aproximado) workers, db_maxconn, RAM/CPU del Postgres y si los crons son pesados (importaciones, conciliaciones, etc.). Con eso te digo qué modo de pooling te conviene y cómo dimensionar el pool para que no te agregue cola/latencia.
Siguiente capítulo ->