Skip to Content

Recommended architecture: Odoo + PgBouncer + PostgreSQL (production-tested pattern)

November 10, 2025 by
Recommended architecture: Odoo + PgBouncer + PostgreSQL (production-tested pattern)
John Wolf
| No comments yet

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:

  1. you clearly separate HTTP vs WebSocket,

  2. you use PgBouncer transaction for the ORM,

  3. you treat the bus (LISTEN) as a special case,

  4. 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)

Next chapter ->

Recommended architecture: Odoo + PgBouncer + PostgreSQL (production-tested pattern)
John Wolf November 10, 2025
Share this post
Tags
Archive
Sign in to leave a comment