cl_waiting is climbing, sv_active just dropped to zero, maxwait is ticking upward. The pattern looks identical to pool exhaustion or a backend failure. But if someone is running planned PostgreSQL maintenance with PAUSE, the metrics are behaving as designed: PAUSE stops new query routing, existing transactions finish, server connections close, and every pending client queues. From the client’s perspective, it looks like an outage. The difference is that it is planned and reversible with RESUME.
PgBouncer has three administrative states that alter traffic flow: PAUSE, DISABLE, and SUSPEND. Each produces a distinct metric signature, and all can trigger saturation alerts if your monitoring does not account for them. The paused and disabled columns in SHOW DATABASES, and the global state visible in SHOW STATE (added in PgBouncer 1.19.0), are the first things to check before escalating any PgBouncer saturation signal.
A PAUSE without a matching RESUME freezes traffic indefinitely. There is no auto-timeout.
What this means
PgBouncer’s administrative commands support controlled maintenance: zero-downtime PostgreSQL upgrades, host cutovers, connection draining before restarts. They are intentional features, not bugs. But because their metric signatures overlap with real failure modes, they are a common source of false-positive pages.
The playbook’s severity definitions for cl_waiting and maxwait both include an explicit condition: the database must NOT be paused or disabled. Without this condition, every planned maintenance window becomes an incident.
Three commands create maintenance states:
PAUSE [db]: Stops routing new queries to server connections. Existing transactions are allowed to complete. Once all server connections are released,
sv_activedrops to zero andcl_waitingspikes as pending clients queue. The command blocks until all server connections are released.RESUME [db]reverses it.DISABLE db: Rejects all new client connections to the specified database. Existing client connections continue working and are NOT closed. This drains traffic by preventing new arrivals while letting in-flight work finish.
ENABLE dbreverses it.SUSPEND: A global command (not per-database) that flushes all socket buffers and stops PgBouncer from listening for data on them. The command blocks until all buffers are empty. New client connections wait. This is the most severe administrative state because it freezes all I/O, including admin commands.
RESUMEreverses it.
flowchart TD
A["Alert: cl_waiting rising, sv_active dropping"] --> B{"SHOW DATABASES: paused = 1?"}
B -- Yes --> C["Maintenance: PAUSE on this db"]
B -- No --> D{"SHOW DATABASES: disabled = 1?"}
D -- Yes --> E["Maintenance: DISABLE on this db"]
D -- No --> F{"SHOW STATE available?"}
F -- Yes --> G{"SHOW STATE = suspended?"}
G -- Yes --> H["Maintenance: SUSPEND (global)"]
G -- "active" --> I["Real incident: investigate further"]
F -- "Pre-1.19.0: check logs" --> J["Grep SUSPEND in pgbouncer log"]
C --> K["Run RESUME db when maintenance is done"]
E --> L["Run ENABLE db to accept new clients"]
H --> M["Run RESUME to unfreeze all I/O"]The three maintenance states compared
| State | Scope | What stops | What keeps working | Metric signature | How to reverse |
|---|---|---|---|---|---|
PAUSE [db] | Per-database | New query routing to server connections | Existing transactions until they complete | sv_active drops to 0, cl_waiting spikes, sv_idle drops to 0 | RESUME [db] |
DISABLE db | Per-database | New client connections | All existing client connections | New connection errors, existing clients unaffected | ENABLE db |
SUSPEND | Global (all databases) | All socket I/O, including admin commands | Nothing new; buffers are flushed | Admin console itself becomes unresponsive | RESUME |
The critical operational difference: PAUSE lets existing clients finish their work before the pool drains. DISABLE lets existing clients keep working indefinitely (new connections are rejected, connected clients can still query). SUSPEND freezes everything.
Quick checks
Run these read-only checks before escalating any PgBouncer saturation alert. Adjust the socket path (-h) and port to match your deployment.
# Check paused/disabled flags per database (reference columns by name, not position)
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -c "SHOW DATABASES;"
# Check global PgBouncer state (requires 1.19.0+)
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -c "SHOW STATE;"
# See which pools have waiting clients and how long they have waited
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -c "SHOW POOLS;"
# Check admin console responsiveness (if this hangs, PgBouncer may be SUSPENDed)
time psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -c "SHOW LISTS;" > /dev/null
# Look for recent administrative commands in the log
grep -i "PAUSE\|DISABLE\|SUSPEND\|RESUME\|ENABLE" /var/log/pgbouncer/pgbouncer.log | tail -20
The paused and disabled columns in SHOW DATABASES output use 1 for active and 0 for normal. Column positions are version-dependent: PgBouncer’s column layouts shift across releases as new fields are added.
On PgBouncer versions before 1.19.0, there is no SHOW STATE command. The only way to detect a global SUSPEND is from the log or by noticing that the admin console itself has become unresponsive.
How to diagnose it
Check
SHOW DATABASESfirst. Look at thepausedanddisabledcolumns for the database showing saturation. Ifpaused = 1, someone ranPAUSEand has not yet runRESUME. Ifdisabled = 1, someone ranDISABLEand has not yet runENABLE.Check
SHOW STATEfor global states. If the output issuspended, PgBouncer is in a globalSUSPEND. This is distinct from per-databasePAUSEand does not show up inSHOW DATABASESflags.Check the log for context. Administrative commands are logged when issued through the admin console. Look for
PAUSE,DISABLE,SUSPEND,RESUME, orENABLEentries to determine who initiated the state and when.Cross-reference with your change calendar. If a PostgreSQL upgrade, host cutover, or connection draining procedure is in progress, the paused state is expected.
If none of the above apply, it is a real incident. The metrics are telling you about a genuine pool exhaustion cascade, backend failure, or connection leak. Proceed with the normal diagnostic path: check
avg_query_timefor backend slowdown, checksv_loginfor backend connectivity, checkSHOW SERVERSfor stuck connections.
Metrics during maintenance vs real outage
Several metric patterns are identical whether PgBouncer is paused or experiencing a real failure. The table below maps the overlapping signals to the distinguishing check.
| Signal | During PAUSE | During real outage | How to tell them apart |
|---|---|---|---|
cl_waiting rising | Yes, all pending clients queue | Yes, same mechanism | SHOW DATABASES: paused = 1 |
sv_active dropping to 0 | Yes, after transactions complete | Possible, if backend is down | Check sv_login: rising in backend failure, zero during PAUSE |
sv_idle dropping to 0 | Yes, connections released during PAUSE | Possible, if pool drained | SHOW DATABASES: paused = 1 |
maxwait increasing | Yes, waiters age in queue | Yes, same | SHOW DATABASES: paused = 1 |
avg_query_time elevated | No, queries are not executing | Possibly, if backend is slow | During PAUSE, query time should be stable or dropping |
| New connections rejected | No (during PAUSE); Yes (during DISABLE) | Yes, if max_client_conn hit or FD exhaustion | SHOW DATABASES: disabled = 1 vs SHOW LISTS: free_clients = 0 |
| Admin console responsive | Yes | Yes (unless event loop stalled) | If admin console hangs, suspect SUSPEND or event loop stall |
The key insight: during PAUSE, avg_query_time and avg_xact_time should not be elevated because queries are not running. If you see high cl_waiting but normal avg_query_time, and the database is paused, the pattern is consistent with maintenance. If you see high cl_waiting with elevated avg_query_time, the backend is the problem regardless of the paused state.
Exiting maintenance states
Each administrative state has a specific reversal command. Running the wrong one does nothing.
Resuming a paused database:
# Resume a specific database
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -c "RESUME mydb;"
If PAUSE was run without a database argument (pausing all databases), RESUME without an argument resumes all.
Enabling a disabled database:
# Re-enable client connections
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -c "ENABLE mydb;"
Resuming from SUSPEND:
# Resume all I/O globally
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -c "RESUME;"
Warning: if PgBouncer is in a SUSPEND state, the admin console itself may be unresponsive because all I/O is frozen, including admin commands. The RESUME command may need to be sent from an already-established admin session or through whatever mechanism your deployment uses for control-plane access.
After running any reversal command, verify the state has cleared:
# Confirm paused = 0 and disabled = 0
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -c "SHOW DATABASES;"
Common pitfalls
No auto-timeout on PAUSE. A PAUSE without a matching RESUME freezes traffic indefinitely. There is no built-in timeout that automatically resumes a paused database. If the operator who ran PAUSE loses their session, forgets, or is interrupted, the database stays paused until someone manually runs RESUME.
Nested PAUSE requires multiple RESUME. If an operator runs PAUSE mydb, then runs PAUSE (all databases), the system may require two separate RESUME commands: one to resume all databases, and one to resume the named database.
PAUSE in session pooling mode may never complete. PAUSE waits for all server connections to be released. In session pooling mode, server connections are only released when the client disconnects. If clients hold long-lived sessions, PAUSE may hang indefinitely waiting for connections that never close.
Concurrent PAUSE and RESUME can deadlock. If RESUME is issued while a PAUSE is still in progress (waiting for connections to drain), the RESUME may return immediately but the PAUSE command may never return, leaving the system in an inconsistent state.
Cross-database queueing after PAUSE/RELOAD. On PgBouncer versions before 1.24.0, running RELOAD between PAUSE and RESUME (a common pattern during host cutovers) may have recycled all server connections across all databases, not just the one being paused. This caused queuing on unrelated databases.
RELOAD is not RESUME. Operators sometimes run RELOAD expecting it to resume traffic after a PAUSE. It does not. RELOAD re-reads the configuration file. RESUME is the only command that reverses a PAUSE.
Prevention
Make paused/disabled state a first-class signal in your monitoring. Every PgBouncer saturation alert for cl_waiting or maxwait must include a condition that checks whether the database is paused or disabled. If it is, suppress the alert. The playbook’s severity definitions already encode this: TICKET only fires if cl_waiting > 0 sustained AND the database is NOT paused/disabled.
Automate the PAUSE/RESUME lifecycle. If you use PAUSE for PostgreSQL maintenance (upgrades, restarts, host cutovers), wrap it in a script that:
- Runs
PAUSE db. - Performs the maintenance.
- Runs
RESUME db. - Verifies that
SHOW DATABASESreportspaused = 0.
If step 3 fails or is skipped, step 4 makes the problem visible immediately.
Document who can run administrative commands. PAUSE, DISABLE, SUSPEND, KILL, and RESUME are available to users listed in admin_users in PgBouncer’s configuration. Restrict this list to the smallest set of operators and automation service accounts. Use stats_users (read-only SHOW commands) for monitoring tools and dashboards.
Prefer DISABLE for connection draining. DISABLE is safer for scenarios where you want to stop new traffic without waiting for existing work to complete. Existing clients keep working, and there is no risk of PAUSE hanging on long-lived sessions in session pooling mode. Use PAUSE only when you need all server connections fully released (for example, before a host cutover where the old backend address will stop responding).
Monitoring integration
The paused and disabled state flags from SHOW DATABASES should be part of any PgBouncer alert condition. Alert rules for cl_waiting or maxwait should suppress when the database is paused or disabled.
Per-second collection of cl_waiting, sv_active, and maxwait lets you pinpoint the exact moment a PAUSE begins (sharp drop in sv_active, immediate spike in cl_waiting) and when RESUME takes effect (queue drains, sv_active returns to baseline).
Correlating the state flag with wait queue metrics in a single dashboard distinguishes planned maintenance from unexpected pool exhaustion without manual SHOW DATABASES queries.
Related guides
- PgBouncer advisory locks in transaction mode: orphaned locks and mysterious contention
- PgBouncer avg_query_time high: reading backend slowdown through the pooler
- PgBouncer avg_wait_time high: the latency the pool itself is injecting
- PgBouncer backend unreachable: PostgreSQL down and the pool draining
- PgBouncer capacity planning: runway for pools, clients, and PostgreSQL slots
- PgBouncer client connection leak: idle clients that never disconnect
- How PgBouncer actually works in production: a mental model for operators
- PgBouncer idle in transaction: the silent pool killer in transaction mode
- PgBouncer LISTEN/NOTIFY not working: why pub/sub needs session pooling
- PgBouncer max_client_conn tuning: setting the client limit against real FD headroom
- PgBouncer maxwait high: the oldest client waiter and how close it is to timing out
- PgBouncer monitoring checklist: the signals every connection pooler needs






