PgBouncer exists to reduce the number of connections PostgreSQL has to hold, so it is easy to assume that adding PgBouncer makes the connection limit problem go away. It does not. PgBouncer is itself a consumer of PostgreSQL connection slots, and its worst-case demand is arithmetic you control in config files: one pool per (database, user) pair, each pool allowed to open pool_size server connections, plus reserve_pool_size overflow, multiplied by every PgBouncer instance pointing at the same backend.
When that worst-case sum exceeds what PostgreSQL can accept, the failure does not show up at steady state. It shows up during the burst: a deploy, a retry storm, a cold start, a batch job. PgBouncer tries to open new server connections, PostgreSQL refuses them with FATAL: sorry, too many clients already, and the pool drains while clients queue. This is a static configuration audit, not a runtime mystery.
This article gives you the arithmetic, the audit procedure, and the signals that tell you the mismatch is about to bite.
The arithmetic that matters
The sum of (pool_size + reserve_pool_size) across every pool, on every PgBouncer instance targeting one PostgreSQL, must stay under max_connections minus superuser_reserved_connections, minus anything else that connects directly.
Written out:
PostgreSQL budget = max_connections
- superuser_reserved_connections
- direct application clients
- replication connections
- admin / ops access headroom
PgBouncer demand = SUM over all instances of:
SUM over all (database, user) pools of:
pool_size + reserve_pool_size
Requirement = PgBouncer demand <= ~80% of max_connections
The ~80% target leaves room for everything that is not PgBouncer: psql sessions during incidents, monitoring, migrations, logical replication, and the superuser connections you will need when the backend is full. If PgBouncer demand can reach 100% of max_connections, then at the exact moment you most need to connect and fix things, you cannot get in.
Three properties of PgBouncer make this sum easy to get wrong:
- Pools multiply by (database, user) pairs, not by database. pool_size is per pool, and a pool exists per unique (database, user) combination. Four users against two databases at default_pool_size of 20 is up to 160 server connections, not 40.
- Per-database and per-user overrides hide. A database stanza can override pool_size, and
max_db_connections/max_user_connectionscap totals across pools. The effective ceiling per instance is not justdefault_pool_sizefrom SHOW CONFIG. - Multiple instances multiply demand silently. Each PgBouncer process, including
so_reuseportsiblings and sidecars, has independent pools. Two instances each sized “safely” at 60% of max_connections are collectively at 120%.
flowchart TD
subgraph pgb1[PgBouncer instance 1]
p1a[pool db1/app1: pool_size + reserve]
p1b[pool db1/app2: pool_size + reserve]
p1c[pool db2/app1: pool_size + reserve]
end
subgraph pgb2[PgBouncer instance 2]
p2a[pool db1/app1: pool_size + reserve]
p2b[pool db2/app2: pool_size + reserve]
end
budget[PostgreSQL max_connections]
reserved[minus superuser_reserved_connections]
direct[minus direct clients and replication]
headroom[~20 percent safety headroom]
p1a --> budget
p1b --> budget
p1c --> budget
p2a --> budget
p2b --> budget
budget --> reserved --> direct --> headroomEvery pool on every instance draws from the same budget. The audit question is whether the sum of the left side fits inside what remains after the right side is subtracted.
What the failure looks like
The failure mode is “backend connection failure with PostgreSQL max_connections reached as the root cause”:
- A burst hits: deploy, cold start, retry storm, or a batch job. Many pools want to grow toward pool_size simultaneously.
- PostgreSQL hits max_connections and refuses new logins. Clients connecting directly to PostgreSQL see
FATAL: sorry, too many clients already. PgBouncer server connections fail withS: login failedin the PgBouncer log. - Inside PgBouncer, sv_login rises or fluctuates (connections attempting and failing), sv_idle declines, and total server connections stop growing even though demand is unmet.
- Existing server connections keep working until they expire (server_lifetime) or error out, so the pool drains gradually rather than dying instantly.
- cl_waiting grows, maxwait climbs, and clients start hitting query_wait_timeout (default 120s) or their own application timeouts and retry, which deepens the queue.
The distinguishing feature versus ordinary pool exhaustion: in ordinary exhaustion, total server connections are stable at pool_size and all are busy. In this failure, the total server connection count is declining or stuck below pool_size while PostgreSQL reports itself full. PgBouncer has headroom on paper and cannot spend it.
One nasty property: this failure is triggered by the burst, not by steady state. Everything looks fine at 40% utilization for months, right up until the day every pool wants its full pool_size at once. That is why it has to be caught by the static audit, not by watching dashboards.
How the mismatch creeps in
- New users or databases added without re-running the sum. Each new (database, user) pair is a whole new pool worth of potential connections.
- A second PgBouncer instance deployed for HA or rollout, sized against the full max_connections as if it were alone.
- reserve_pool_size treated as free. Reserve connections are real PostgreSQL connections. They must be in the sum.
- pool_size set to 0 to mean “use the default” out of habit. Since PgBouncer 1.24.0, a pool_size of 0 is documented to mean unlimited, which removes the per-pool ceiling entirely; older versions treated 0 differently. Check your version before assuming 0 is safe.
- max_db_connections and max_user_connections left at 0 (unlimited), so nothing caps runaway demand across pools for a hot database or user.
- PostgreSQL max_connections lowered during a “right-sizing” exercise while PgBouncer configs were left alone.
Auditing the arithmetic
This is a read-only procedure. Run it per PostgreSQL instance, and re-run it after any change to users, databases, PgBouncer instance count, or pool configuration.
1. Get the PostgreSQL budget.
# On PostgreSQL itself
psql -Atc "SHOW max_connections;"
psql -Atc "SHOW superuser_reserved_connections;"
# Current usage for context
psql -Atc "SELECT count(*) FROM pg_stat_activity;"
2. Get per-database pool configuration from every PgBouncer instance.
# Run against each PgBouncer instance's admin console
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -Atc "SHOW DATABASES;"
SHOW DATABASES gives you per-database pool_size, reserve_pool_size, and max_connections (the per-database cap, 0 means unlimited). This is the authoritative view because it includes per-database overrides of default_pool_size.
3. Enumerate the actual pools.
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -Atc "SHOW POOLS;"
Each row is one (database, user) pool. Caveat: pools are created on demand. SHOW POOLS shows pairs that have connected; a configured user that has not connected yet has no pool row but will create one under load. For the worst case, count the (database, user) combinations your applications can use, not just the ones currently visible.
4. Compute worst-case demand per instance.
For each instance, for each possible pool: min(pool_size + reserve_pool_size, max_db_connections if set, max_user_connections if set). Sum across pools. Then sum across all instances targeting this PostgreSQL.
5. Compare against the budget.
demand <= 0.8 * max_connections
and demand <= max_connections - superuser_reserved_connections - direct_clients - replication_slots_in_use
If either check fails, you are one burst away from login refusals. Also sanity-check SHOW CONFIG for default_pool_size: if it is 0 and you run PgBouncer 1.24.0 or newer, that may mean unlimited, and the per-pool ceiling is gone.
Signals to watch
The audit is the primary defense, but these runtime signals tell you the ceiling is being approached or has been hit:
| Signal | Why it matters | Warning sign |
|---|---|---|
| PostgreSQL connection count vs max_connections | The actual budget consumption | Sustained above 80% of max_connections |
| sv_login per pool (SHOW POOLS) | Server connections stuck or failing in the login phase | Persistently > 0 with cl_waiting growing |
| Total server connections vs pool_size | Whether PgBouncer can reach its configured ceiling | Total declining or pinned below pool_size during a burst |
| cl_waiting and maxwait (SHOW POOLS) | Client impact once server connections cannot be established | cl_waiting > 0 sustained, maxwait > 15s |
PgBouncer log: S: login failed | Direct evidence of backend login refusals (log-only, no SHOW counter exists) | Any occurrence during load |
PostgreSQL log: FATAL: sorry, too many clients already | The limit itself being hit | Any occurrence |
| Reserve pool activation | Overflow being drawn, meaning base pools are at ceiling | Usage sustained beyond brief spikes |
PgBouncer has no error counters in its SHOW commands. The login refusal itself is only visible in logs, so the metric signals (sv_login, cl_waiting, pool totals) are your leading indicators and the logs are confirmation.
Keeping demand under the limit
Once the audit shows over-subscription, the levers, roughly in order of preference:
- Shrink pool_size to match real concurrency. Most pools need far fewer server connections than their defaults suggest. Use sv_active history: if a pool never exceeds 8 active connections, a pool_size of 20 is pure risk, not capacity.
- Set max_db_connections and max_user_connections. These cap total server connections per database and per user across all pools, turning unbounded multiplication into a bounded one. They are 0 (unlimited) by default.
- Consolidate users. Every extra (database, user) pair is a pool. Fewer application roles means fewer pools and a smaller worst case.
- Raise PostgreSQL max_connections only deliberately. Each connection costs backend memory, and a high limit invites the very pile-up PgBouncer exists to prevent. Raising the limit to make room for oversized pools is usually the wrong direction.
- Treat reserve_pool_size as part of the budget, not as slack. If reserve usage is sustained, grow the base pool within budget or reduce demand; do not let the reserve mask chronic undersizing.
- Never rely on pool_size = 0 on 1.24.0+. Unlimited pools make the entire audit meaningless.
If you are currently in the incident (PostgreSQL full, PgBouncer login failures), the fastest relief is freeing slots on the PostgreSQL side: terminate idle or runaway direct connections, and reduce demand by pausing non-critical database stanzas in PgBouncer (PAUSE dbname) rather than restarting anything. A PgBouncer restart during this failure makes it worse: every pool re-establishes connections at once, producing a login storm against a backend that is already full.
How Netdata helps
- PostgreSQL connection utilization against max_connections as a first-class chart, so budget consumption is visible before the FATAL errors start, per backend.
- PgBouncer pool state collection from the admin console (sv_active, sv_idle, sv_login, cl_waiting, maxwait per pool), letting you see when total server connections stall below pool_size while the backend is full: the signature of this failure.
- Correlation between PgBouncer sv_login spikes and PostgreSQL connection saturation on one dashboard, which turns “is the pooler broken or is the database full?” into a glance instead of a two-terminal investigation.
- Per-pool breakdowns so one hot (database, user) pair does not hide inside a healthy aggregate.
- Alerting on approach, not just arrival: warn when PostgreSQL connection usage crosses a sustained percentage of max_connections, while there is still time to run the audit and shrink pools.
Related guides
- PgBouncer capacity planning: runway for pools, clients, and PostgreSQL slots
- PgBouncer backend unreachable: PostgreSQL down and the pool draining
- PgBouncer avg_wait_time high: the latency the pool itself is injecting
- PgBouncer maxwait high: the oldest client waiter and how close it is to timing out
- PgBouncer max_client_conn tuning: setting the client limit against real FD headroom
- PgBouncer no more connections allowed (max_client_conn): the front door is full
- PgBouncer monitoring checklist: the signals every connection pooler needs
- How PgBouncer actually works in production: a mental model for operators






