Your alert fired on sv_active / pool_size > 85%, or you spotted the ratio creeping up on a dashboard. No clients are waiting yet. cl_waiting is zero. Latency looks normal. This is exactly the moment this signal exists for: it is the last cheap warning you get before the pool goes over the cliff.
PgBouncer pool saturation is not a gradual degradation. Below 100% utilization, client wait time is approximately zero because server connection assignment is instant. At 100%, the next client request has nowhere to go and enters a FIFO queue with unbounded wait. There is no “slow but working” middle state. The ratio of sv_active to pool_size tells you how close you are to that edge, and it moves before cl_waiting, maxwait, and avg_wait_time show anything.
This article covers what the ratio means, why it degrades as a cliff, which checks to run, and how to decide whether to act now or just watch.
What this means
PgBouncer maintains one server connection pool per (database, user) pair. sv_active counts server connections currently linked to a client, executing a query or holding a transaction. pool_size (from SHOW DATABASES) is the cap on server connections for that pool. When sv_active equals pool_size, the pool is full: the next client request that needs a server connection must wait.
The invariant that governs the pool (absent reserve pool use) is:
sv_active + sv_idle + sv_used + sv_tested + sv_login <= pool_size
sv_active is the consumed capacity. sv_idle is the ready reserve: connections connected to PostgreSQL and immediately usable. The other states are transient inventory (recently returned connections awaiting reuse, connections being tested, connections logging in). When sv_active rises, sv_idle shrinks. When sv_idle hits zero and sv_active hits pool_size, queuing begins.
The operating bands:
| Ratio | State | Action |
|---|---|---|
| < 70% | Healthy headroom | None. Baseline capacity planning only. |
| 70-85% | Watch | Trend it. Identify what is consuming connections. |
| > 85% sustained | Act | Diagnose the cause before the pool fills. |
| 100% with waiters | Incident | See PgBouncer pool exhaustion. |
flowchart LR
A[sv_active / pool_size rising] --> B{ratio?}
B -->|under 70%| C[healthy headroom]
B -->|70-85%| D[watch: find the consumer]
B -->|over 85% sustained| E[act: diagnose cause]
B -->|100% sustained| F[cl_waiting grows, maxwait climbs]
F --> G[app timeouts, retry cascade]Two properties of this signal matter more than the number itself.
Per-pool beats aggregate. Pools are per (database, user). One pool at 100% while five others idle produces a global average that looks fine. If you chart sum(sv_active) / sum(pool_size) across pools, a saturated pool is invisible. Always alert and chart per pool, not on the global aggregate.
The pool mode changes what the ratio means. In transaction pooling mode, server connections are acquired and released per transaction, so sv_active fluctuates rapidly. Brief spikes to 1.0 under burst traffic are normal; sustained 1.0 with cl_waiting > 0 is the incident. In session mode, sv_active reflects active sessions and moves slowly, so a rising ratio there is a capacity trend, not noise.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Slow backend queries | sv_active rising, avg_query_time above baseline, connections held longer per query | SHOW STATS_AVERAGES query_time vs baseline |
| Long or idle-in-transaction holds | avg_xact_time much larger than avg_query_time, sv_active high with modest query rate | Ratio of xact_time to query_time; PostgreSQL pg_stat_activity for idle in transaction |
| Traffic growth | sv_active tracking query rate up over days or weeks, everything else normal | SHOW STATS_AVERAGES query_count trend |
| Undersized pool_size | Ratio routinely above 85% at normal peak, occasional brief cl_waiting blips | Peak sv_active vs pool_size over a week |
| Connection leak (session mode) | sv_active climbing without matching traffic, connections never released | SHOW SERVERS connect_time and request_time on active connections |
| Cold start or recycling wave | sv_login elevated, pool churning, ratio temporarily high | SHOW POOLS sv_login; recent restart or server_lifetime boundary |
The two you most need to separate are “the database got slower” and “the pool got too small.” They produce the same sv_active curve but opposite fixes. That is what avg_query_time and avg_wait_time are for; see the diagnosis steps.
Quick checks
All read-only, run against the admin console.
# Per-pool snapshot: sv_active, sv_idle, cl_waiting, maxwait
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -Atc "SHOW POOLS;"
# Configured pool_size and administrative state (paused / disabled) per database
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -Atc "SHOW DATABASES;"
# Backend latency and wait latency, per database
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -Atc "SHOW STATS_AVERAGES;"
# Per-connection detail: which server connections are old
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -Atc "SHOW SERVERS;"
Notes on reading these:
- In
SHOW POOLSunaligned output on PgBouncer 1.21+,cl_waitingis the 4th field,sv_activethe 7th,sv_loginthe 13th,maxwaitthe 14th. Older versions lack the cancel-related fields, which shifts these positions. Column positions inSHOW STATSare also version-dependent (1.23 addedserver_assignment_count, 1.24 added prepared statement counters). PreferSHOW STATS_AVERAGESand reference columns by name, not position. paused = 1ordisabled = 1inSHOW DATABASESchanges the meaning of everything else. DuringPAUSE,cl_waitingspikes andsv_activedrains to zero by design. Check this first before escalating.- A single
SHOW POOLSis a point-in-time snapshot. Sample it a few times over a minute before drawing conclusions about “sustained.”
How to diagnose it
Identify the pool. From
SHOW POOLS, find which(database, user)rows havesv_activenearpool_size. If you only track the aggregate, stop and fix that first; per-pool is where this signal lives.Confirm it is sustained, not a burst. In transaction mode, brief 1.0 spikes are normal. Sample
SHOW POOLSseveral times over 1-5 minutes. Sustained high ratio, or a ratio that has been trending up over days, is what warrants action.Split wait time from query time. From
SHOW STATS_AVERAGES, comparequery_timeandwait_timefor the affected database:query_timeelevated above baseline: the backend is slow. Connections are held longer, so the pool fills. The fix is on PostgreSQL, not PgBouncer.query_timenormal, ratio still high: the pool is undersized for current concurrency, or transactions are holding connections without querying.
Check for idle-in-transaction holds. If
xact_timeis much larger thanquery_time, clients are holding server connections while doing non-database work. In transaction mode this is the most common silent cause of highsv_active. Cross-check on PostgreSQL:# Find idle-in-transaction backends holding pool connections psql -h <postgres-host> -U <user> -d <db> -c \ "SELECT pid, state, now() - xact_start AS xact_age, query FROM pg_stat_activity WHERE state = 'idle in transaction' ORDER BY xact_age DESC;"Trace the hold to a client. In
SHOW SERVERS, active connections have alinkcolumn pointing at the client using them, andrequest_timeshowing how long the current work has run. Follow the link intoSHOW CLIENTSto get the source address of the application holding the connection.Check headroom explicitly. Look at
sv_idlefor the pool.sv_idle = 0withcl_waiting = 0is the “looks green, is actually yellow” state: no one is waiting, but the next request when all connections are active will queue. Zero idle at peak means you are one slow query from an incident.Check reserve pool usage if configured. If
reserve_pool_size > 0, compare total server connections in the pool (sv_active + sv_idle + sv_used + sv_tested + sv_login) againstpool_size. Totals abovepool_size, or"taking connection from reserve_pool"in the log, mean the base pool is already overflowing. Reserve is for brief spikes; sustained use meanspool_sizeis chronically undersized.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
sv_active / pool_size per pool | Leading saturation indicator; moves before clients queue | >85% sustained for >5 minutes |
sv_idle per pool | Ready reserve; zero means no headroom | 0 sustained, or trending toward 0 over days |
cl_waiting per pool | Clients blocked now; the trailing confirmation of saturation | Any sustained nonzero with maxwait growing |
maxwait per pool | Age of the oldest waiter; user-facing pain | >5s noticeable, >15s likely app failures |
avg_wait_time (SHOW STATS) or wait_time (SHOW STATS_AVERAGES) | Pooling-induced latency | Sustained above zero; >100ms is serious |
avg_query_time (SHOW STATS) or query_time (SHOW STATS_AVERAGES) | Backend speed as seen by PgBouncer | >2x baseline: root cause is PostgreSQL |
avg_xact_time vs avg_query_time | Gap measures idle-in-transaction hold time | xact_time much larger than query_time |
| Reserve pool usage | Overflow engaged; base pool undersized | Total server connections > pool_size, or reserve log lines |
paused / disabled per database | Context that inverts alert meaning | Suppress other alerts when set |
Fixes
Backend is slow (avg_query_time elevated)
The pool is a symptom, not the cause. Fix the query or the database: missing indexes, lock contention, I/O saturation. Canceling a single long-running query on PostgreSQL (pg_cancel_backend) can free a held pool connection, but coordinate with the application team first and treat it as a tactical move, not the fix. Do not grow pool_size to compensate for a slow database; that multiplies load against an already struggling backend and consumes more max_connections slots on PostgreSQL.
Idle-in-transaction holds
Fix the application pattern: do not hold BEGIN open across HTTP calls or computation. As a guardrail, PostgreSQL’s idle_in_transaction_session_timeout can auto-terminate these, which releases the PgBouncer server connection as a side effect, but clients receive an error, so roll it out deliberately.
Pool genuinely undersized
Raise pool_size for the affected database (per-database override or default_pool_size) and RELOAD. Check two budgets before you do:
- The sum of all pools’
pool_size(plus reserve, if configured) across every PgBouncer instance targeting this PostgreSQL must fit insidemax_connectionswith room for direct connections, replication, and admin access. Keep PgBouncer capacity under about 80% ofmax_connections. - File descriptors: each server connection is an FD. Confirm the process is nowhere near its limit (compare
Max open filesin/proc/<pid>/limitsagainstls /proc/<pid>/fd | wc -l).
Do not shrink the pool because sv_idle is high
Idle server connections in transaction mode are the ready reserve, not waste. Reducing pool_size because sv_idle “looks unused” removes burst capacity and moves the cliff closer. The correct response to high idle with low waiting is nothing, or slower capacity planning, not a smaller pool.
Prevention
- Alert per pool on the ratio, with duration.
sv_active / pool_size > 85%sustained for 5 minutes, per(database, user). Do not alert on the global aggregate, and do not alert on instantaneous spikes: transaction-mode bursts hit 1.0 legitimately. - Gate alerts on administrative state. Every PgBouncer saturation alert must check
paused/disabledinSHOW DATABASESand suppress during maintenance. Highcl_waitingduringPAUSEis expected behavior. - Trend headroom, not just utilization. Chart
sv_idleper pool over weeks. A linear trend toward zero gives you runway:(pool_size - current sv_active) / daily growth rateis roughly your days-to-queuing estimate. - Pair wait time and query time on every dashboard. They are the attribution pair. One tells you the pool is too small, the other tells you the database is too slow. Never look at either alone.
- Audit applications for transaction-mode hazards. Idle-in-transaction patterns and long multi-statement transactions are application bugs that present as pool saturation.
avg_xact_time / avg_query_timeis the cheap detector. - Size against PostgreSQL first. Pool capacity planning is constrained by
max_connectionson the backend, not by how many connections PgBouncer can technically hold.
How Netdata helps
- Per-pool
sv_activevspool_sizeas a ratio chart, so the 70/85/100% bands are visible per(database, user)rather than hidden in a global average. cl_waitingandmaxwaiton the same timeline as the ratio, so you can see the leading indicator (ratio rising) and the trailing confirmation (waiters appearing) in one view and judge how much runway you have.avg_wait_timenext toavg_query_time, which is the attribution split: pooling-induced latency versus backend latency. This is the correlation that prevents the wrong fix.sv_idletrending, turning “zero headroom at peak” from an incident discovery into a capacity ticket weeks earlier.- Anomaly detection on per-pool utilization, which catches the single-pool-at-100% case that aggregate dashboards and static thresholds on averages miss.
- Paused/disabled state collected alongside saturation metrics, giving alerts the maintenance context they need to avoid paging during a planned
PAUSE.






