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_active drops to zero and cl_waiting spikes 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 db reverses 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. RESUME reverses 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

StateScopeWhat stopsWhat keeps workingMetric signatureHow to reverse
PAUSE [db]Per-databaseNew query routing to server connectionsExisting transactions until they completesv_active drops to 0, cl_waiting spikes, sv_idle drops to 0RESUME [db]
DISABLE dbPer-databaseNew client connectionsAll existing client connectionsNew connection errors, existing clients unaffectedENABLE db
SUSPENDGlobal (all databases)All socket I/O, including admin commandsNothing new; buffers are flushedAdmin console itself becomes unresponsiveRESUME

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

  1. Check SHOW DATABASES first. Look at the paused and disabled columns for the database showing saturation. If paused = 1, someone ran PAUSE and has not yet run RESUME. If disabled = 1, someone ran DISABLE and has not yet run ENABLE.

  2. Check SHOW STATE for global states. If the output is suspended, PgBouncer is in a global SUSPEND. This is distinct from per-database PAUSE and does not show up in SHOW DATABASES flags.

  3. Check the log for context. Administrative commands are logged when issued through the admin console. Look for PAUSE, DISABLE, SUSPEND, RESUME, or ENABLE entries to determine who initiated the state and when.

  4. Cross-reference with your change calendar. If a PostgreSQL upgrade, host cutover, or connection draining procedure is in progress, the paused state is expected.

  5. 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_time for backend slowdown, check sv_login for backend connectivity, check SHOW SERVERS for 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.

SignalDuring PAUSEDuring real outageHow to tell them apart
cl_waiting risingYes, all pending clients queueYes, same mechanismSHOW DATABASES: paused = 1
sv_active dropping to 0Yes, after transactions completePossible, if backend is downCheck sv_login: rising in backend failure, zero during PAUSE
sv_idle dropping to 0Yes, connections released during PAUSEPossible, if pool drainedSHOW DATABASES: paused = 1
maxwait increasingYes, waiters age in queueYes, sameSHOW DATABASES: paused = 1
avg_query_time elevatedNo, queries are not executingPossibly, if backend is slowDuring PAUSE, query time should be stable or dropping
New connections rejectedNo (during PAUSE); Yes (during DISABLE)Yes, if max_client_conn hit or FD exhaustionSHOW DATABASES: disabled = 1 vs SHOW LISTS: free_clients = 0
Admin console responsiveYesYes (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:

  1. Runs PAUSE db.
  2. Performs the maintenance.
  3. Runs RESUME db.
  4. Verifies that SHOW DATABASES reports paused = 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.