Configuring PgBouncer pool_size and max_client_conn

This guide is part of PgBouncer Transaction vs Statement Pooling. Choosing a pooling mode is the first decision; sizing the pools is the second, and it is the one that determines whether PgBouncer absorbs your connection load or becomes the bottleneck itself.

PgBouncer has two independent sides, and almost every sizing mistake comes from conflating them. On the client side it accepts connections from your applications, and that number can be very large. On the server side it opens connections to PostgreSQL, and that number must stay inside the database’s ceiling. The whole point of the proxy is that these two numbers differ by an order of magnitude.

The settings that matter

Setting Side What it bounds Default
max_client_conn client total client connections PgBouncer will accept across all pools 100
default_pool_size server server connections per (database, user) pool 20
min_pool_size server server connections kept warm per pool 0
reserve_pool_size server extra server connections lent to a pool under pressure 0
max_db_connections server hard cap on server connections to one database, across all pools 0 (unlimited)
max_user_connections server hard cap on server connections for one user, across all pools 0 (unlimited)

The default max_client_conn of 100 is the setting that catches everyone. It is a global cap across every pool, not per database, and PgBouncer refuses new clients with no more connections allowed (max_client_conn) once it is reached. For a proxy whose purpose is to accept thousands of clients, 100 is far too low; it exists as a conservative default, not a recommendation.

Client side and server side are bounded separately Many application clients connect into PgBouncer on the left, bounded by max_client_conn. A much smaller number of server connections leave PgBouncer on the right, bounded by default_pool_size and max_db_connections against the PostgreSQL max_connections ceiling. Large on the client side, small on the server side — by design application clients 120 pods x 20 connections = 2400 client connections max_client_conn = 5000 cheap: a socket and a small buffer each, no backend process PgBouncer multiplexes clients onto a small server pool queues clients when the pool is lent out PostgreSQL max_connections = 200 max_db_connections = 160 expensive: one backend process, work_mem and cache per connection headroom left for admin sessions The ratio between the two numbers is the entire value the proxy delivers. If they are close, you have added a network hop and gained nothing.
PgBouncer's job is to make the client-side number large and the server-side number small. Both sides need their own limits.

Sizing the server side

Server-side sizing starts at the database, not at PgBouncer. Work down from max_connections:

  1. Start with the database ceiling. Suppose max_connections = 200 and superuser_reserved_connections = 3.
  2. Subtract operational headroom. Reserve 20–30 connections for migrations, monitoring, replication slots, and a human with psql during an incident. That leaves roughly 170.
  3. Set max_db_connections below that. With one PgBouncer instance, max_db_connections = 160 guarantees the proxy can never consume the whole ceiling, no matter how many pools exist.
  4. Divide across pools. Each distinct (database, user) combination is its own pool with its own default_pool_size. Five such pools at default_pool_size = 25 is 125 server connections — comfortably inside 160.
  5. Divide again by PgBouncer instance count. If you run three PgBouncer replicas behind a load balancer, each one gets a third of the budget. This is the step most often skipped, and it is why a working single-instance configuration blows past max_connections the moment it is made highly available.
[databases]
orders = host=db.internal port=5432 dbname=orders pool_size=25
reports = host=db.internal port=5432 dbname=orders user=reporter pool_size=8

[pgbouncer]
pool_mode = transaction
max_client_conn = 5000
default_pool_size = 25
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
max_db_connections = 160
max_user_connections = 60
server_idle_timeout = 600
server_lifetime = 3600

The per-database pool_size= override in the [databases] section is how you give a reporting workload a smaller share than the transactional one. Without it, every pool inherits default_pool_size and a batch job can occupy as many server connections as the request path.

The right value for default_pool_size in transaction mode is much smaller than intuition suggests. In transaction pooling a server connection is only held for the duration of a transaction, so a pool of 25 can serve thousands of clients whose transactions are a few milliseconds each. Sizing it toward the database’s CPU and disk parallelism — rather than toward client count — is the correct instinct, and the arithmetic is the same as in Connection Pool Sizing Formulas.

Sizing the client side

max_client_conn should be comfortably above the sum of every application pool’s maximum, plus headroom for reconnect churn during a deploy. During a rolling restart both the old and new pods hold connections briefly, so the peak is higher than steady state.

max_client_conn  >=  (pods x pool max)  x  1.5   +  admin sessions
                  =  (120 x 20)         x  1.5   +  20
                  =  3620  ->  round up to 5000

Client connections are cheap in PgBouncer — a socket, a small buffer, and an entry in an internal list. The practical constraint is file descriptors: each client connection uses one, and PgBouncer needs max_client_conn + max_db_connections + a small constant descriptors available. Raise the process limit accordingly, or PgBouncer will fail to accept connections with accept() failed: Too many open files long before max_client_conn is reached:

# systemd unit
[Service]
LimitNOFILE=16384

PgBouncer also allocates pkt_buf bytes per connection (2048 by default), so 5000 clients is roughly 10 MB of buffers — negligible. Raising pkt_buf to 8192 for large result sets multiplies that by four; still small, but worth knowing before setting it to something exotic.

What a raised client limit actually costs Each client connection consumes a file descriptor and a packet buffer. Raising max_client_conn without raising the process descriptor limit causes accept failures well before the configured limit is reached. Raising the client limit requires raising the descriptor limit with it max_client_conn 5000 the configured client ceiling plus server connections one descriptor each file descriptors needed above 5200, so set 16384 memory cost 5000 x 2 KB buffers, about 10 MB Without the descriptor increase you get accept() failures with Too many open files long before the client limit binds. The memory cost is small; the descriptor limit is the constraint that actually bites.
Client connections are cheap in memory but each needs a file descriptor. Raise the process limit alongside.

reserve_pool_size and min_pool_size

These two are frequently left at their defaults of zero, and both are useful.

min_pool_size keeps a floor of warm server connections per pool. Without it, a pool that goes fully idle closes all its server connections, and the next request pays a full PostgreSQL connection setup — process fork, authentication, catalog load — inside its own latency. Setting min_pool_size = 5 on a pool with pool_size = 25 costs five idle backends and removes that cliff. Note that min_pool_size only applies while the pool has been used at least once; PgBouncer does not pre-warm pools at startup.

reserve_pool_size is an overflow allowance. When clients have been waiting longer than reserve_pool_timeout seconds, PgBouncer allows the pool to grow by up to reserve_pool_size extra server connections. It converts a hard queue into a soft one for brief spikes. The trap is that reserve connections count against max_db_connections too, so the true worst case per pool is pool_size + reserve_pool_size, and your arithmetic in the previous section must use that number.

Stacking the server-side limits A vertical budget showing the PostgreSQL max_connections ceiling at the top, operational headroom below it, max_db_connections below that, and individual pools with their reserve allowances stacked inside the max_db_connections allocation. Every pool's worst case is pool_size plus its reserve PostgreSQL max_connections = 200 — exceeding this rejects every new connection headroom — 40 max_db_connections = 160 — PgBouncer's hard cap for this database orders pool 25 + 5 reserve = 30 reports pool 8 + 5 reserve = 13 room for further pools 117 remaining Running three PgBouncer replicas triples the server-side total unless each replica's max_db_connections is divided by three. This is the most common way a highly available deployment exhausts a ceiling that a single instance respected.
Budget downward from max_connections, count reserves, and divide by the number of PgBouncer instances.

Common failure patterns and remediation

Symptom Underlying cause Remediation
no more connections allowed (max_client_conn) Global client cap left at the default 100 Raise max_client_conn above the fleet’s total pool maximum with margin
accept() failed: Too many open files File descriptor limit below max_client_conn Raise LimitNOFILE above clients plus server connections
FATAL: sorry, too many clients already from PostgreSQL Server side unbounded, or replicas each using the full budget Set max_db_connections; divide it by the PgBouncer instance count
High cl_waiting with low database CPU default_pool_size too small for the transaction rate Raise pool_size for that pool, or shorten transactions
High cl_waiting with saturated database CPU Pool is correctly sized; the database is the bottleneck Do not raise pool_size — it will make latency worse
Latency spike on the first request after a quiet period min_pool_size at zero, pool fully collapsed Set min_pool_size to a small non-zero value
Brief spikes cause timeouts despite headroom elsewhere No reserve configured Set reserve_pool_size and a short reserve_pool_timeout
One tenant starves the others All pools share default_pool_size and no per-user cap Set per-database pool_size= and max_user_connections

Verifying the configuration

SHOW POOLS on the admin console is the authoritative view. The columns that matter for sizing are cl_active, cl_waiting, sv_active, sv_idle, and maxwait:

psql -h 127.0.0.1 -p 6432 -U pgbstats pgbouncer -c "SHOW POOLS;"

Read it this way. sv_active at pool_size with cl_waiting at zero is a correctly sized pool at full utilisation — not a problem. sv_active at pool_size with cl_waiting above zero and maxwait growing means clients are queuing, and the next question is whether the database is saturated. If PostgreSQL CPU is low, raise pool_size; if it is high, the pool is already the correct size and raising it converts queue time into contention. Reading these columns in depth is covered in PgBouncer Metrics Monitoring.

SHOW DATABASES shows the effective pool_size, reserve_pool and max_connections per database after all overrides are applied, which is the fastest way to confirm a config reload actually took effect:

psql -h 127.0.0.1 -p 6432 -U pgbstats pgbouncer -c "SHOW DATABASES;"

Operational boundary

These settings decide how many connections exist and how many clients may queue. They do not make queries faster, and pool_size is not a throughput dial you can turn upward indefinitely. Past the point where the database’s CPUs and disks are saturated, a larger pool increases concurrency without increasing work done, and total latency rises. Confirm the database has spare capacity before raising pool_size, using the server-side view in PostgreSQL Server-Side Connection Diagnostics.

Note also that these limits are per PgBouncer process. PgBouncer is single-threaded, so scaling past roughly 10,000–20,000 client connections or saturating one CPU core means running multiple instances — at which point every server-side number must be divided among them.

Frequently Asked Questions

Is max_client_conn per database or global?
Global. It caps total client connections across every pool in the instance. Per-database client limits are set with the max_db_client_connections setting or by giving each database its own PgBouncer instance.
What is a pool, exactly?
One pool per unique (database, user) pair. Ten application users connecting to the same database create ten pools, each with its own pool_size. This is why max_db_connections exists — it caps the sum.
Should pool_size be larger in session mode?
Yes, substantially. In session pooling a server connection is held for the client’s entire session, so pool_size must be at least the number of concurrent clients. That is why session mode gives up most of the multiplexing benefit; see PgBouncer Transaction vs Statement Pooling.
Do reserve connections count against max_db_connections?
Yes. Budget each pool as pool_size + reserve_pool_size when checking the total against the database ceiling.
Why does my application pool also need a limit?
Because the application pool’s maximum is what determines client connection count. A pool with max: 200 per pod is what drives max_client_conn upward. Keeping application pools small is still worthwhile even behind a proxy — it bounds queue depth at the layer closest to the request.
How do I change these without dropping connections?
Edit pgbouncer.ini and issue RELOAD; on the admin console. Most settings, including pool_size and max_client_conn, apply to new connections without disrupting existing ones. Changing pool_mode is the notable exception and warrants a maintenance window.
Does min_pool_size pre-open connections at startup?
No. PgBouncer maintains the floor only after the pool has seen its first client. A freshly started proxy still pays connection setup on the first request.