Alerting on PgBouncer Client and Server Limits

This guide is part of PgBouncer Metrics Monitoring. PgBouncer enforces several limits, and each one fails differently. max_client_conn refuses new clients outright; default_pool_size makes them wait; max_db_connections and max_user_connections cap backends across pools. Teams usually alert on waiting clients and discover the others during an incident, when the log says something they have never seen:

WARNING C-0x55c9: (nodb)/(nouser)@10.4.2.31:52134 pooler error: no more connections allowed (max_client_conn)
WARNING C-0x55d1: orders/orders_app@10.4.2.44:41002 pooler error: server login has been failing
LOG C-0x55e2: orders/orders_app@10.4.2.12:33120 closing because: query_wait_timeout (age=15s)

The first is a hard refusal that looks like a network failure to the application. The objective is an alert for each limit, with thresholds that fire while there is still room to act.

Rapid incident diagnosis

Read every limit and its current usage in one pass. The console’s views cover them:

SHOW CONFIG;      -- max_client_conn, default_pool_size, max_db_connections, max_user_connections
SHOW LISTS;       -- databases, users, pools, free_clients, used_clients, free_servers, used_servers
SHOW POOLS;       -- per pool: cl_active, cl_waiting, sv_active, sv_idle, sv_used, maxwait
SHOW DATABASES;   -- per database: pool_size, max_connections, current_connections

SHOW LISTS is the one that answers the client-limit question directly: used_clients approaching the sum of free and used is the approach to max_client_conn.

Match the symptom to the limit:

Symptom Limit Metric to watch
Clients cannot connect at all; no more connections allowed max_client_conn pgbouncer_clients_* totals, or used_clients from SHOW LISTS
Clients connect but queries wait default_pool_size cl_waiting, maxwait
Waiting persists although the pool looks small max_db_connections current_connections per database
One user waits while others are fine max_user_connections per-user pools
Server logins failing authentication or the database’s own limit ConnERR-equivalent counters, PostgreSQL log

Check whether the database is the real limit. PgBouncer cannot open more backends than PostgreSQL allows:

SELECT current_setting('max_connections')::int AS limit,
       count(*) AS in_use,
       current_setting('max_connections')::int - count(*) AS free
FROM pg_stat_activity;

Identify which pool. With several databases and users, the limits are per pool, and an aggregate view hides which one is short. Always group by database and user.

PgBouncer limits and how each one fails max_client_conn refuses new clients, default_pool_size makes queries wait, max_db_connections caps backends per database and max_user_connections caps them per user. max_client_conn new clients refused outright default_pool_size queries wait; cl_waiting rises max_db_connections waiting although the pool looks small max_user_connections one user waits, others unaffected Limit → symptom
Four limits, four different failures. Only the second one produces the waiting clients most dashboards watch.

Mathematical sizing or parameter formula

Client limit headroom. Clients are cheap, so the limit should sit well above the fleet’s maximum:

peak_clients = Σ over services ( replicas × workers × pool_max )
max_client_conn ≥ peak_clients × 1.5 + admin_and_tools

With 40 processes holding 10 connections each, peak clients is 400, and max_client_conn of 1,000 leaves room for a deploy overlap and ad-hoc sessions. Alert at 70 % of the limit:

warn_at = 0.7 × max_client_conn

Server pool headroom. The backend pool should be sized from concurrent transactions, and the alert placed where waiting starts to be sustained:

default_pool_size = ceil( transactions_per_second × mean_transaction_seconds ÷ 0.7 )
warn when: cl_waiting > 0 for longer than a scrape interval, or maxwait > 1 s

Database and user caps. These are the totals the database must accept:

Σ over pools ( pool_size ) ≤ max_db_connections ≤ database max_connections − reserved

An alert on current_connections per database at 85 % of max_db_connections catches the case where several pools share one host.

Memory. Each client connection costs PgBouncer memory for its buffers:

memory ≈ (max_client_conn × pkt_buf × 2) + (server_connections × pkt_buf × 2) + overhead

With the default 4 KB packet buffer and 1,000 clients, that is a few tens of megabytes — small, but worth checking before raising max_client_conn into the tens of thousands.

Headroom for client and server limits The client limit of one thousand sits well above the fleet's peak of four hundred, while the backend pool of eleven sits just above the measured concurrent transaction count of eight. Configured limit against measured peak clients 1,000 backends 11 filled: measured peak. outline: configured limit. plenty of headroom on clients, deliberately little on backends
Client connections are cheap and server connections are not. The two limits therefore deserve very different headroom.

Exact remediation & configuration

Export the metrics. The Prometheus exporter publishes the console’s views; scrape it per PgBouncer instance with an instance label:

scrape_configs:
  - job_name: pgbouncer
    scrape_interval: 15s
    static_configs:
      - targets: ["10.0.1.20:9127", "10.0.1.21:9127", "10.0.1.22:9127"]
        labels: { service: orders }

Alert on each limit. The rules below assume the exporter’s common metric names; adjust to the exporter in use:

groups:
  - name: pgbouncer-limits
    rules:
      # 1. Client limit: refusals are invisible to PgBouncer's pool metrics, so watch the total
      - alert: PgBouncerClientLimitApproaching
        expr: sum by (instance) (pgbouncer_pools_client_active_connections
                               + pgbouncer_pools_client_waiting_connections)
              > 0.7 * on (instance) pgbouncer_config_max_client_conn
        for: 5m
        labels: { severity: warning }
        annotations:
          summary: "{{ $labels.instance }}: client connections above 70 % of max_client_conn"

      # 2. Pool saturation: waiting clients sustained
      - alert: PgBouncerClientsWaiting
        expr: max by (instance, database) (pgbouncer_pools_client_waiting_connections) > 0
        for: 2m
        labels: { severity: warning }

      # 3. Wait time: the page-worthy signal
      - alert: PgBouncerMaxwaitHigh
        expr: max by (instance, database) (pgbouncer_pools_client_maxwait_seconds) > 2
        for: 1m
        labels: { severity: critical }

      # 4. Per-database cap
      - alert: PgBouncerDatabaseConnectionsHigh
        expr: pgbouncer_databases_current_connections
              > 0.85 * pgbouncer_databases_max_connections
        for: 5m
        labels: { severity: warning }

      # 5. The exporter itself
      - alert: PgBouncerExporterDown
        expr: pgbouncer_up == 0
        for: 5m
        labels: { severity: warning }

Alert on refusals from the log where the metric does not exist. max_client_conn refusals are logged but not always exported; a log-based counter closes the gap:

# promtail / vector rule
- match: 'no more connections allowed'
  labels: { alert_source: pgbouncer_client_limit }

Raise the right limit. Each symptom has one correct response:

[pgbouncer]
max_client_conn = 1000            ; raise when clients are refused and memory allows
default_pool_size = 11            ; raise when clients wait and the database has capacity
max_db_connections = 30           ; the cap across pools on one database
max_user_connections = 30         ; the cap per user
query_wait_timeout = 15           ; bound the wait rather than letting it grow
reserve_pool_size = 4
reserve_pool_timeout = 2

Apply with RELOAD; on the admin console — none of these requires a restart.

Keep the totals consistent. The sum of per-instance max_db_connections across every PgBouncer must fit the database’s usable connections, as covered in Deploying Highly Available PgBouncer.

Dashboards that make the limits obvious

Each limit should appear on the dashboard as a line, not as a number in a runbook.

Panel one: clients against the client limit. A stacked series of active and waiting clients with max_client_conn drawn as a threshold. Refusals happen at the line, and the distance to it is the headroom.

Panel two: server connections against the pool size. sv_active + sv_idle + sv_used per pool with pool_size + reserve_pool_size as the threshold shows whether the reserve is in use — a sign the regular pool is undersized.

Panel three: maxwait. The single most informative series PgBouncer publishes, because it is measured in the units users experience.

Panel four: per-database totals. current_connections against max_connections per database entry, which catches the cap that is easiest to forget.

Add two annotations: configuration reloads and deploys. A step change in any of these panels almost always follows one or the other.

Panel Threshold line What crossing it means
Clients max_client_conn New clients are being refused
Server connections per pool pool_size + reserve The reserve is compensating
maxwait 1–2 seconds Users are visibly waiting
Per-database connections max_db_connections Pools on this host are capped

Keep the limits in the dashboard, not only in the config. Exporting the configuration as metrics — most exporters publish pgbouncer_config_* series — means the threshold lines update themselves when someone changes a limit, which removes the classic failure where the dashboard still shows last year’s number.

Four panels, four limits Clients against max_client_conn, server connections against pool size plus reserve, maxwait against a latency threshold, and per-database connections against max_db_connections. 1 · Clients vs max_client_conn 2 · Server connections vs pool_size + reserve 3 · maxwait vs 1–2 s 4 · Per-database vs max_db_connections Pair every measurement with its limit export the configuration as metrics so the threshold lines stay correct
Each panel pairs a measurement with the limit that bounds it. A dashboard without the limit lines requires the viewer to remember four numbers.

Validation & verification

Each alert fires in a rehearsal. In staging, drive each limit in turn: open clients beyond 70 % of max_client_conn; hold transactions to fill the pool; lower max_db_connections below the pool sum. Each should fire its own alert and no others.

The client-limit alert precedes refusals. With the alert at 70 %, there should be several minutes between it firing and the first no more connections allowed entry in the log.

Thresholds track the configuration. Change a limit with RELOAD and confirm the dashboard’s threshold line moves, which proves the configuration metrics are being scraped.

Per-instance labels are present. With several PgBouncer instances, every alert must identify which one; an alert that aggregates across instances hides the one that is short.

The exporter’s own failure is covered. Stop an exporter and confirm PgBouncerExporterDown fires — otherwise every other alert silently stops working.

Frequently Asked Questions

Why does max_client_conn refusal not show in the pool metrics?
The connection never reaches a pool: PgBouncer rejects it during the handshake, so it appears in the log and in the totals rather than in cl_active or cl_waiting. That is why the client-limit alert uses the totals and a log rule.
Should max_client_conn simply be set very high?
It costs memory per client and removes a backstop against a runaway client fleet. A limit well above the expected peak, with an alert at 70 %, is better than a limit so high it can never be reached.
What is the difference between sv_used and sv_idle?
sv_idle are server connections ready for immediate use; sv_used have been used and are waiting for the server_check_delay before being reused. Both are available capacity; only sv_active is genuinely busy.
Do these alerts change with so_reuseport?
Yes — each process has its own limits and its own metrics, so alerts must be per process. Summing across processes hides a single saturated one, as described in Scaling PgBouncer Across CPU Cores with so_reuseport.