The "real interaction" betweenpooling(for example PgBouncer) and theOdoo ORMis better understood if you look at it as three overlapping layers:
Request / Job(HTTP or cron)
ORM(env/cr/cursor + transaction)
PostgreSQL Connection(with pooling in front)
Below, I will break it down with the actual flow and the points where it usually breaks or degrades.
1) What Odoo really does: cursor = transaction
In Odoo, the ORM works on env, and env.cr is acursorpsycopg2 associated with aconnection. In practice:
Each request (or cron job) enters a worker.
Odoo creates acursor(cr) and starts atransaction.
The ORM executes SQL through that cursor (with prefetch/cache at the ORM level, but always within that transaction).
In the end:
COMMITif all is OK
ROLLBACKif there is an exception
The cursor is closed and the connection returns to thepool(Odoo pool or external pool).
Key:For Odoo, a "unit of work" is usuallyone transaction per request(or per cron job), except in special cases (sub-transactions with savepoints).
2) Odoo already has internal pooling (although many people underestimate it)
Odoo maintains aconnection pool per process(each worker). This means:
A worker does not necessarily openafixed connection: it can open several up to db_maxconn (Odoo parameter).
In production, it is common for each worker to use few connections, butduring peaksit can grow.
If you have workers = N, the worst case of connections to Postgres can approach:
N * db_maxconn(+ auxiliary connections, cron, etc.)
That’s why PgBouncer is so popular: it “flattens” that peak towards Postgres.
3) PgBouncer: why the pooling mode changes EVERYTHING
PgBouncer has 3 modes (the ones that matter here):
A) pool_mode = session (the most compatible)
A “client” maintains the same Postgres backend throughout the session.
Odoo “believes” that its connection is stable (and it is).
Fewer surprises with session state.
✅ It is the mode that usually “always works” with Odoo.
B) pool_mode = transaction (tempting, but with traps)
PgBouncer assigns a Postgres backendonly during the transaction.
When doing COMMIT/ROLLBACK, the backend returns to the pool and the next request may hitanotherbackend.
This can go wellif and only ifyour app does not depend onsession stateoutside of the transaction.
Where Odoo can get complicated in transaction pooling:
Session advisory locks(pg_advisory_lock, pg_try_advisory_lock): if Odoo takes locks that depend on "the session", and PgBouncer changes the backend for you, that lock may end up on a different backend than expected.
Anything that depends on "I stay on the same backend" outside of a single transaction.
There are deployments that make it work, but this is where "ghost" bugs appear (crons that overlap, locks that are not respected, strange behavior under load).
C) pool_mode = statement
Almost never for Odoo. Too aggressive.
4) "Real interaction" ORM ↔ pooling: the timeline that matters
Typical HTTP request (with workers)
Worker receives request
Odoo takesa connection(from the internal pool)
Opens cr (cursor) → starts transaction
ORM performs reads/writes (prefetch/cache does not change the fact that you are within that transaction)
COMMIT/ROLLBACK
Returns connection to the pool
With PgBouncer session pooling:that "step 2" ends up in a stable Postgres backend.
With PgBouncer transaction pooling:the Postgres backend is only guaranteedfor 3–5. On the 6th, the backend is released.
5) The most important point: "session state" vs "transaction state"
For PgBouncer in transaction to be safe, your app must comply with:
"Everything I need lives within the transaction; I do not assume session continuity."
Odoo, in general, is quite close to that for most HTTP traffic (because request=transaction), but there are two areas where it can hurt:
Locks: if something uses session locks and expects stability, transaction pooling makes them unpredictable.
Jobs/cron: tend to perform long operations and coordination between processes; if there are session locks or 'connection coordination', transaction pooling can break invariants.
That's why the practical recommendation is usually:
If you want zero surprises:PgBouncer session.
If you need to maximize connection reduction:transaction, but specifically testing crons/locks/real concurrency.
6) Useful numbers: how to size 'real pooling'.
Let's assume:
workers = 12
db_maxconn = 8
Without PgBouncer (worst case):up to ~96 connections to Postgres from Odoo.
With PgBouncer:you can limit 'to Postgres' with default_pool_size/max_db_connections (depending on your setup), and accept many 'client' connections from Odoo to PgBouncer, while PgBouncer multiplexes.
The key is that, even if you reduce connections to Postgres,you cannot reduce 'effective concurrency'without impacting latency: if you have 12 workers that need the DB and only 10 backends, 2 will have to wait.
7) Practical recommendation for Odoo (straightforward)
'Normal' production (ERP + crons + various modules):PgBouncer in session.
If you go for transaction, do it knowing that you are buying complexity:
test thoroughlycronswith load
test scenarios oflocks / contention
measure if you really needed transaction or if with session + good sizing you were already fine
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.