If you are going to scale Odoo seriously (more concurrent users, more jobs, more integrations), the bottleneck almost always ends up beingthe connections to PostgreSQLand how you manage them. The combinationOdoo + PgBouncer + PostgreSQLis the de facto standard… but only if you set it up correctly (especially regarding theLISTEN/NOTIFYof the bus and the/websocket/).
Here you have a clear, robust, and easy-to-operate architecture.
Objective of this architecture
Reduce actual connectionsto PostgreSQL without killing concurrency.
Avoid 'Connection Pool Is Full'and latency spikes.
Maintain compatibility withreal-time notifications(bus) andWebSocket.
FacilitateHA / horizontal scalingon the Odoo side.
Recommended diagram (simple and scalable)
Internet
|
[ Nginx/HAProxy ]
TLS + gzip + limits
/websocket/ | /
| | /
v v v
[ Odoo gevent ] [ Odoo workers ]
:8072 :8069
\ /
\ /
v v
[ PgBouncer (transaction) ]
:6432
|
v
[ PostgreSQL ]
:5432
Key idea:
Odoo 'normal' (ORM/HTTP) goes to PgBouncertransaction pooling.
The channel ofWebSocketgoes to gevent_port and is routed separately in the proxy.
Why PgBouncer intransaction pooling
Odoo makes a lot of short transactions (reads, small writes). With pool_mode = transaction:
You efficiently reuse 'server' connections (PgBouncer→Postgres).
You accept many clients (workers) without inflating max_connections in Postgres.
You improve latency under load.
But: transaction pooling does NOT work for 'session' things (and that's where the bus comes in).
The real 'gotcha': LISTEN/NOTIFY (bus) and PgBouncer
Odoo uses notifications for real-time events (bus). In PostgreSQL, LISTEN isper session. In PgBouncertransactionyour 'session' changes: today you listen, tomorrow you are moved to another connection. Result: the bus becomes unstable.
Correct solutions (choose 1)
Option A (recommended):Odoo normal → PgBouncer (transaction) andbus/listen directly to Postgres
Simpler, fewer pieces.
Requires separating that connection (by config/module/tuning according to your version and stack).
Option B:2 PgBouncer
pgbouncer_tx (transaction) for normal Odoo
pgbouncer_sess (session) only for LISTEN
More complex, but everything goes through PgBouncer.
Base configuration by component
1) Nginx (reverse proxy)
Separating /websocket/ is mandatory if you use gevent/websocket:
upstream odoo_http {
server 127.0.0.1:8069;
}
upstream odoo_ws {
server 127.0.0.1:8072;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
# TLS config here...
# WebSocket
location /websocket/ {
proxy_pass http://odoo_ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Rest of Odoo
location / {
proxy_pass http://odoo_http;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 600s;
proxy_connect_timeout 30s;
proxy_send_timeout 600s;
}
}
In Odoo: proxy_mode = True.
2) Odoo (production)
Golden points:
workers > 0 (multi-process)
proxy_mode = True
Normal DB pointing to PgBouncer
Separate WebSocket (gevent port)
Example:
[options] proxy_mode = True workers = 8 max_cron_threads = 2 http_port = 8069 gevent_port = 8072 db_host = 127.0.0.1 db_port = 6432 db_user = odoo db_password = ******** db_maxconn = 64
db_maxconn is per process: if you increase it thoughtlessly and have many workers, you can stress PgBouncer and Postgres.
3) PgBouncer (transaction pooling)
Practical example:
[databases] odoodb = host=127.0.0.1 port=5432 dbname=odoodb [pgbouncer] listen_addr = 0.0.0.0 listen_port = 6432 auth_type = scram-sha-256 auth_file = /etc/pgbouncer/userlist.txt pool_mode = transaction max_client_conn = 2000 default_pool_size = 50 reserve_pool_size = 20 reserve_pool_timeout = 5 server_idle_timeout = 60 server_reset_query = DISCARD ALL ignore_startup_parameters = extra_float_digits
Quick notes:
default_pool_size = actual "server" connections per db/user.
reserve_pool_size helps during short spikes.
DISCARD ALL clears the session when the client changes (recommended with transaction pooling).
4) PostgreSQL
Practical rules:
max_connections must cover:
the "server" connections that PgBouncer will open (pool_size + reserve)
direct connections (monitoring, maintenance, direct bus if applicable)
In pg_hba.conf, don't forget to allow the PgBouncer and Odoo host as appropriate, and keep it closed to the Internet.
How to size (without magic)
1) Odoo workers
A useful guide:
workers ≈ (CPU cores * 2) + 1 (adjust for actual load and RAM)
2) PgBouncer default_pool_size
If Postgres is on the same host and your DB is fast: 20–80 usually works.
If there is a lot of latency or heavy queries: smaller pool and optimize queries/indexes.
3) Postgres max_connections
Think this way:
max_connections >= (default_pool_size + reserve_pool_size) * (db/user pools) + margin
If you only have 1 db and 1 user: it's simple. If you have several, add them up.
Two final architectures "ready for production"
Architecture 1 (recommended)
1 PgBouncer (transaction)
Direct bus/listen to Postgres (only that connection)
Proxy separating websocket to gevent
✅ Better simplicity/stability ratio.
Architecture 2 (enterprise/strict)
PgBouncer TX (transaction) for normal Odoo
PgBouncer SESS (session) for bus/listen
Postgres behind, no bypass
✅ Everything goes through proxy/poolers, but increases complexity.
Observability: the minimum you should monitor
In PgBouncer
SHOW POOLS; (queue, active, waiting)
SHOW STATS; (latency, tx/s)
In PostgreSQL
pg_stat_activity (states, waits)
top queries (pg_stat_statements if you use it)
In Odoo
latency of endpoints
cron queues
pool/timeout errors
Checklist for "well-done architecture"
Nginx routes /websocket/ to gevent_port
proxy_mode = True
Normal Odoo → PgBouncer pool_mode=transaction
Bus/listen does NOT depend on transaction pooling (bypass or session pooling)
default_pool_size and max_connections calculated (not random)
reasonable limits and timeouts in proxy and pooler
secure authentication (SCRAM), and closed network between components
Closure
The Odoo + PgBouncer + PostgreSQL architecture works excellently when:
you clearly separate HTTP vs WebSocket,
you use PgBouncer transaction for the ORM,
you treat the bus (LISTEN) as a special case,
you size pools with numbers, not with faith.
If you want, I can provide youwith an exact proposal of values(workers, db_maxconn, default_pool_size, max_connections) if you tell me:
CPU/RAM of the server
if Postgres is on the same host or remote
concurrencia objetivo (usuarios simultáneos + crons pesados + integraciones)