You opened SHOW POOLS because an application is slow, and one column stands out: maxwait is 8, 20, maybe 90 seconds. That number is the age of the oldest client sitting in PgBouncer’s FIFO wait queue, computed from the query_start of the first waiter. It is the worst-case queuing latency any client is experiencing right now, before its query even reaches PostgreSQL.
maxwait is more actionable than the cl_waiting count. cl_waiting tells you how many clients are blocked; maxwait tells you how much it hurts. A queue of 50 clients that drains in 200ms is a burst. A queue of 3 clients where the oldest has waited 45 seconds is an incident.
The metric matters because of what happens at the end of it. PgBouncer disconnects a waiter when its wait exceeds query_wait_timeout (default 120 seconds), so maxwait is also a countdown to the oldest client being killed with an error. If your application has a shorter timeout than 120s, it gives up and retries first, and those retries join the back of the queue and make everything worse.
What this means
When all server connections in a pool are busy, new client queries enter a per-(database, user) FIFO queue. maxwait (seconds, plus maxwait_us for the microsecond remainder) measures the head of that queue. Zero means nobody is waiting. Anything sustained above zero means the pool cannot keep up with demand.
Operational heuristics that match what most OLTP applications experience:
| maxwait | What it usually means |
|---|---|
| 0 | No queuing. Healthy. |
| >1s | Noticeable added latency. Investigate if sustained. |
| >5s | Impacting user experience. Applications with tight SLAs are degrading. |
| >15s | Likely causing application timeouts and retries. Treat as an incident. |
approaching query_wait_timeout (default 120s) | The oldest waiter is about to be disconnected with an error. |
Two nuances before you escalate:
- maxwait counts from when the query was sent, not from when the client connected. A client connected for hours in session mode contributes nothing to maxwait until it submits a query that has to queue.
- Check paused/disabled state first. During an administrative
PAUSE <db>, new queries queue while existing transactions finish, socl_waitingandmaxwaitspike by design.DISABLErejects new clients but lets existing ones work. Both look alarming and are expected during maintenance.SHOW DATABASESexposespausedanddisabledcolumns; check them before treating any queue depth as an outage.
flowchart TD
A[maxwait rising] --> B{SHOW DATABASES: paused or disabled?}
B -- yes --> C[Expected maintenance behavior. Do not escalate.]
B -- no --> D{sv_active at pool_size?}
D -- yes --> E{avg_query_time elevated?}
E -- yes --> F[Backend slow: connections held too long. Go to PostgreSQL.]
E -- no --> G[Pool undersized or idle-in-transaction holding slots.]
D -- no --> H{sv_login elevated, total server connections declining?}
H -- yes --> I[Backend connection failure: auth, network, DNS, or max_connections.]
H -- no --> J[Check reserve pool, event loop CPU, and per-pool breakdown.]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Pool size too small for the workload | sv_active pinned at pool_size, sv_idle at 0, queue grows during every traffic peak | avg_query_time: if it is low, the backend is fine and the pool is undersized |
| Slow queries on PostgreSQL | avg_query_time elevated 2x+ over baseline, connections returned slowly, maxwait climbs | SHOW STATS_AVERAGES for avg_query_time; then pg_stat_activity on the backend |
| Idle-in-transaction clients | sv_active at pool_size, but avg_xact_time much larger than avg_query_time | SHOW SERVERS: active connections with old request_time; pg_stat_activity shows idle in transaction |
| Backend connection failure | sv_login elevated, total server connections declining, sv_idle draining to zero | PgBouncer log for “connect failed”, “S: login failed”, “server DNS lookup failed” |
| Long transactions in session mode | A few clients hold server connections for minutes; queue builds behind them | SHOW SERVERS request_time, identify the linked client via SHOW CLIENTS |
| Administrative PAUSE | cl_waiting spikes, sv_active drains to zero, paused = 1 in SHOW DATABASES | SHOW DATABASES paused/disabled columns |
| Event loop saturation | All pools slow at once, admin console itself sluggish, PgBouncer CPU near 100% of one core | time psql ... -c "SHOW LISTS" for admin console latency; process CPU |
| Reserve pool masking an undersized base pool | Total server connections exceed pool_size; log shows “taking connection from reserve_pool” | Compare sum of sv_* per pool against pool_size from SHOW DATABASES |
Quick checks
All commands below are read-only and safe to run during an incident. They assume the admin console on port 6432 with the pgbouncer admin database; adjust host, port, and user for your deployment.
# Full pool snapshot: cl_waiting, maxwait, sv_active, sv_idle, sv_login per pool
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -Atc "SHOW POOLS;"
# Maintenance context FIRST: paused and disabled columns
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -Atc "SHOW DATABASES;"
# Separate pool wait from backend latency (values in microseconds)
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -Atc "SHOW STATS_AVERAGES;"
# Per-client wait detail: who is waiting and for how long (wait, wait_us columns)
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -Atc "SHOW CLIENTS;"
# Per-server detail: which connections are stuck and how long they have been active
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -Atc "SHOW SERVERS;"
# Confirm the actual timeout the oldest waiter is racing against
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -Atc "SHOW CONFIG;" | grep -E "query_wait_timeout|pool_size|reserve_pool"
# Event loop health: admin console should answer in well under 100ms
time psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -c "SHOW LISTS;" > /dev/null
# Error evidence: PgBouncer exposes zero error counters via SHOW; logs are the only source
grep -E "query_wait_timeout|connect failed|login failed|reserve_pool" /var/log/pgbouncer/pgbouncer.log | tail -30
Column positions in SHOW POOLS and SHOW STATS output have changed across releases as new counters were added. Reference columns by name in any scripted check, not by field index.
How to diagnose it
Rule out maintenance. Run
SHOW DATABASESand checkpausedanddisabled. If the affected database is paused, the queue is intentional. Coordinate with whoever ranPAUSEonRESUMEtiming. Do not escalate further.Confirm the queue is real and sustained. Poll
SHOW POOLSa few times, 10 to 15 seconds apart. A single snapshot showingcl_waiting > 0with maxwait under a second is a burst, especially in transaction pooling mode where the pool turns over quickly. Sustained maxwait above 5 seconds is where you start treating it as an incident.Identify which pool is hurting. maxwait and cl_waiting are per
(database, user). One saturated pool can sit next to many healthy ones. Note the database and user of the worst pool; everything after this is scoped to it.Split pool wait from database time. In
SHOW STATS_AVERAGES, compareavg_wait_timeagainstavg_query_timefor that database. High wait with low query time means PostgreSQL is fast and the pool is undersized or connections are being held idle. High query time means the backend is the root cause and PgBouncer is the messenger. This single comparison decides which half of the system you debug next.Check supply vs demand. Compare
sv_activein SHOW POOLS againstpool_sizein SHOW DATABASES. If sv_active equals pool_size and sv_idle is 0, the pool is fully saturated. If sv_active is well below pool_size while clients queue, server connections are not being created: look at sv_login, per-databasecurrent_connectionsvsmax_connectionsin SHOW DATABASES, and the log for connection failures.Hunt the connection holders. If sv_active is at pool_size, run
SHOW SERVERSand look for active connections with the oldestrequest_time. Thelinkcolumn maps each server connection to a client; cross-reference withSHOW CLIENTSto find the source address. On the PostgreSQL side, checkpg_stat_activityforidle in transactionstate. The signature of idle-in-transaction starvation isavg_xact_timefar larger thanavg_query_time.Check whether the backend is failing, not slow. If total server connections (sv_active + sv_idle + sv_used + sv_tested + sv_login) decline over successive polls while sv_login stays elevated, PgBouncer cannot establish new backend connections. Check
SHOW DNS_HOSTSfor stale or unresolvable addresses, test a direct connection from the PgBouncer host to PostgreSQL, and check whether PostgreSQL is atmax_connections. Resizing the pool will not help this pattern.Estimate time to first casualty. Compare current maxwait against
query_wait_timeoutfrom SHOW CONFIG (default 120s) and against your application’s own timeout, whichever is shorter. That gap is how long you have before clients start erroring or retrying. PgBouncer logs aquery_wait_timeoutevent when it disconnects a waiter, so grep the log to see whether casualties have already started.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
| maxwait / maxwait_us (SHOW POOLS) | Age of the oldest waiter; the direct measure of user-facing queuing pain | Sustained >5s; any approach toward query_wait_timeout |
| cl_waiting (SHOW POOLS) | Queue depth per pool; required context for maxwait | Non-zero sustained beyond a traffic burst |
| sv_active vs pool_size | Leading indicator: at 100%, the next request queues | Ratio >85% sustained; 100% with waiters |
| sv_idle | Ready reserve; zero means one slow query away from a queue | 0 sustained, even with cl_waiting = 0 |
| avg_wait_time (SHOW STATS_AVERAGES) | Typical queuing delay PgBouncer injects; complements the maxwait worst case | Sustained >100ms, or any creep above a zero baseline |
| avg_query_time vs avg_wait_time | Splits “waiting for pool” from “waiting for database” | wait_time high while query_time is low |
| avg_xact_time vs avg_query_time | Exposes idle-in-transaction connection hoarding | xact_time many times larger than query_time |
| sv_login | Backend connection establishment health | Consistently >0 and rising, with cl_waiting growing |
| paused / disabled (SHOW DATABASES) | Maintenance context that invalidates every other alert | Must be checked before any escalation |
| Reserve pool usage | Overflow activation means the base pool is undersized | Server connections above pool_size for >5 minutes |
| Log events: query_wait_timeout, connect failed | Only source of error evidence; no SHOW counters exist | Any query_wait_timeout events in production |
Averaging caveat: SHOW STATS averages roll over the stats period and SHOW POOLS is a point-in-time snapshot, so sub-second queue spikes between polls are invisible. That is acceptable here; sub-second queuing is not the problem this article is about.
Fixes
If the backend is slow (high avg_query_time)
The pool is a symptom. Fix the queries: find the offenders via pg_stat_activity and pg_stat_statements on PostgreSQL, and cancel or terminate the worst blockers to free server connections. Coordinate before killing anything that might be a batch job. PgBouncer-side changes only shuffle the queue.
If the pool is undersized (low avg_query_time, sv_active = pool_size)
Raise pool_size (or default_pool_size) for the affected database and run RELOAD. The arithmetic: required pool size is roughly transactions_per_second x avg_xact_time_in_seconds. Respect the ceiling: the sum of all pool sizes across all PgBouncer instances targeting one PostgreSQL must stay comfortably below max_connections, leaving room for superuser and direct connections. Do not shrink pool_size because sv_idle looks high; in transaction mode, idle connections are the burst buffer, not waste.
If idle-in-transaction clients are hoarding connections
This is an application bug, not a PgBouncer tuning problem. Identify the client via SHOW SERVERS link and SHOW CLIENTS addr, and fix the code that opens a transaction and then does non-database work. As a safety net, PostgreSQL’s idle_in_transaction_session_timeout can auto-terminate these sessions, but that changes application-visible behavior, so roll it out deliberately.
If backend connections cannot be established
Do not touch the pool size. Fix reachability: verify PostgreSQL is up and below max_connections, check credentials in PgBouncer’s auth configuration, and check SHOW DNS_HOSTS for a stale address after a failover. A RELOAD refreshes the DNS cache.
If reserve pool is absorbing the overflow regularly
The reserve pool (reserve_pool_size, default 0) exists for brief spikes: it only kicks in after a client has waited reserve_pool_timeout (default 5s). If it activates routinely, the base pool is chronically undersized. Raise pool_size instead of growing the reserve; when base plus reserve both fill, the queuing cliff is deeper.
On query_wait_timeout itself
The 120s default is far longer than most application timeouts, which means applications time out and retry long before PgBouncer ejects the waiter, amplifying the queue. Many operators lower query_wait_timeout to a value aligned with the application’s own timeout so that PgBouncer, not the application, enforces the queue limit. Tradeoff: a shorter timeout disconnects legitimate waiters during transient backend slowness instead of letting them ride it out. Change it with the application timeout in mind, not in isolation.
Prevention
- Alert on sustained maxwait, not on cl_waiting > 0. Brief queuing during bursts is normal in transaction mode and makes
cl_waiting > 0a false-positive machine. The signal that distinguishes “burst” from “stuck” is maxwait above a threshold (5s is a reasonable starting point) sustained for a minute or more, with a paused/disabled check built into the alert condition. - Suppress alerts during administrative state. High cl_waiting with sv_active draining to zero is the exact signature of both a PAUSE and a real outage. Every PgBouncer alert must read the paused/disabled columns first.
- Track headroom, not just pain. Trend sv_active / pool_size per pool. Above 85% sustained, act before the queue forms. Zero sv_idle with zero waiters is a yellow state, not green: you are one slow query from a cascade.
- Baseline avg_wait_time and avg_xact_time. A creep from zero wait time, or a growing gap between transaction time and query time, are the early warnings that arrive weeks before the maxwait incident.
- Align timeouts across layers. Application timeout,
query_wait_timeout, and load balancer timeouts in front of PgBouncer should be a deliberate stack, not three independent defaults. - Watch capacity relationships. Sum of pool sizes versus PostgreSQL max_connections, and max_client_conn versus the file descriptor limit, are misconfigurations that turn a traffic peak into a maxwait incident.
- Remember that stats reset on restart and that SHOW commands expose no error counters. Log collection for PgBouncer is not optional if you want query_wait_timeout events and connection failures after the fact.
How Netdata helps
- Netdata collects PgBouncer pool metrics continuously through the admin console, so maxwait, cl_waiting, and per-pool server states become a time series instead of a snapshot you happened to catch during the incident.
- Per-pool breakdowns show which
(database, user)pool owns the queue while the aggregate still looks healthy. - Correlating client wait time with query time on the same dashboard makes the “pool too small” vs “database too slow” decision visual instead of a manual two-query comparison.
- The gap between transaction time and query time, tracked over weeks, surfaces idle-in-transaction creep before it saturates a pool.
- Alerting on sustained maxwait with anomaly detection on wait time catches slow-building cases that static cl_waiting thresholds miss, without paging on normal burst queuing.
- Host-level metrics (per-core CPU for the single-threaded event loop, file descriptor usage) sit next to the pooler metrics, which matters when the root cause is PgBouncer itself rather than the pools.






