Applications fail with connection errors, but PgBouncer’s pools look healthy: sv_active is well below pool_size, cl_waiting is zero, and PostgreSQL is idle. The log tells the real story: no more connections allowed (max_client_conn) or accept failed: Too many open files. Clients are being refused before they ever reach a pool.

The usual root cause is a mismatch between two limits operators treat as one. max_client_conn is a configuration value. The OS file-descriptor limit (ulimit -n) is a hard kernel ceiling. PgBouncer cannot accept more client connections than it has file descriptors for, no matter what the config says. If you set max_client_conn = 10000 while the process runs with the default 1024 FD limit, PgBouncer lowers the value at startup or hits the FD ceiling under load and refuses connections long before you expect it to.

This guide covers how to compute the real limit, verify what the running process actually has, and close the gap.

What this means

Every connection PgBouncer manages costs file descriptors:

  • One FD per client connection.
  • One FD per server connection to PostgreSQL.
  • Listening sockets (TCP and Unix socket).
  • The log file.
  • Internal pipe FDs.
  • Admin console sockets.
  • DNS resolution sockets.

The effective client capacity is therefore not max_client_conn. It is:

effective_limit = min(max_client_conn, fd_limit - server_connections - overhead)

where overhead covers listening sockets, the log file, pipes, admin sockets, and DNS. A sizing rule of thumb that accounts for both:

fd_limit >= max_client_conn x 2 + 500

The “x 2” covers the server connection each active client will typically need, and the 500 covers non-connection FDs plus safety margin.

Two more facts that change the math:

  • Admin connections to the special pgbouncer database are exempt from max_client_conn. You can always get in to run SHOW commands even when clients are being refused.
  • PgBouncer may lower max_client_conn at startup based on the detected FD limit. The config file is not authoritative; the running process is.
flowchart TD
  A[max_client_conn from config] --> C{Effective client capacity}
  B[OS FD limit from ulimit or systemd] --> D[Subtract server connections]
  D --> E[Subtract listen, log, pipe, admin, DNS FDs]
  E --> C
  C --> F[Clients accepted up to this point]
  F --> G[New connections refused: no more connections allowed]
  E -. exceeded .-> H[EMFILE: Too many open files, possible crash-loop]

Common causes

CauseWhat it looks likeFirst thing to check
max_client_conn raised without raising the FD limitRefusals start well below the configured limit, or the config was lowered at startupSHOW CONFIG value vs grep "Max open files" /proc/<pid>/limits
Default ulimit (1024) never changedPgBouncer refuses connections around a few hundred clients despite a four-digit config/proc/<pid>/limits on the running process
systemd unit missing LimitNOFILElimits.conf was edited but the service still runs with a low limit/proc/<pid>/limits, not ulimit -n in your shell
Application connection leakused_clients climbs steadily toward the real limit under normal loadSHOW CLIENTS for connections with very old connect_time
Retry storm after an incidentused_clients spikes during pool exhaustion as applications reconnectPgBouncer log for refusal bursts correlated with cl_waiting events

Quick checks

All read-only. Run against the admin console and the live process.

# 1. Find the PgBouncer PID
pgrep -f pgbouncer

# 2. Check the ACTUAL FD limit of the running process (not your shell's ulimit)
grep "Max open files" /proc/$(pgrep -f pgbouncer)/limits

# 3. Count currently open FDs
ls /proc/$(pgrep -f pgbouncer)/fd | wc -l

# 4. Check the runtime max_client_conn (as loaded, not as written in the ini)
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -Atc "SHOW CONFIG;" | grep max_client_conn

# 5. Check current client usage against the limit
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -Atc "SHOW LISTS;" | grep -E "used_clients|free_clients"

# 6. Look for refusal and FD errors in the log
grep -c "no more connections allowed" /var/log/pgbouncer/pgbouncer.log
grep -c "Too many open files" /var/log/pgbouncer/pgbouncer.log

# 7. Sum current server connections (they consume FDs too)
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -Atc "SHOW LISTS;" | grep -E "used_servers|free_servers"

Check 2 is the one people skip. Your interactive shell’s ulimit -n tells you nothing about a service started by systemd or an init script. Only /proc/<pid>/limits reflects what the process actually has.

How to diagnose it

  1. Establish the three numbers. From the quick checks: the loaded max_client_conn (from SHOW CONFIG), the process FD limit (from /proc/<pid>/limits), and the current FD count. If SHOW CONFIG reports a lower max_client_conn than your ini file, PgBouncer lowered it at startup because the FD limit could not support your value.

  2. Compute the budget. Take the FD limit. Subtract total server connections (used_servers plus headroom for pool growth up to the sum of all pool_size values). Subtract roughly 500 for listening sockets, the log file, pipes, admin sockets, and DNS. What remains is your real client capacity. Compare it to max_client_conn.

  3. Check whether FDs or config is the binding constraint. If the open FD count is near the FD limit while used_clients is well below max_client_conn, FD exhaustion is refusing connections, not the config. This is the worse failure mode: EMFILE can also block server connections and log writes, and the process can crash-loop.

  4. If the limit is real and being reached legitimately, find the consumers. Run SHOW CLIENTS and group by source address and connect_time. Connections with very old connect_time and no recent activity point to an application leak. A broad distribution of fresh connections points to genuine concurrency growth or a retry storm.

  5. Check for a retry cascade. If refusals coincide with cl_waiting > 0 and rising maxwait in SHOW POOLS, the sequence is pool exhaustion first, application timeouts and retries second, client-slot exhaustion third. Fix the pool problem; the client limit is a symptom. See the pool exhaustion guide linked below.

Metrics and signals to monitor

SignalWhy it mattersWarning sign
used_clients / max_client_connProximity to the hard refusal wall>80% sustained; >95% is urgent
Open FD count vs /proc/<pid>/limitsThe real ceiling, which may be lower than the configFD usage >80% of limit
PgBouncer log: no more connections allowedActive refusal. Only visible in logs, not in any SHOW commandAny sustained rate
PgBouncer log: Too many open filesFD exhaustion, risk of crash-loop and lost log FDAny occurrence
used_clients trend over weeksCapacity runway for client slotsSteady climb without traffic explanation
SHOW CLIENTS old connect_time entriesLeaked connections consuming slots indefinitelyLong-lived idle connections accumulating

Fixes

Raise the FD limit to match, then set max_client_conn below it

This is the correct fix when you genuinely need more client capacity. Do both sides together; changing only one recreates the mismatch.

On systemd-managed hosts, set the limit on the unit, not in limits.conf. Systemd does not apply /etc/security/limits.conf to services:

# /etc/systemd/system/pgbouncer.service.d/override.conf
[Service]
LimitNOFILE=21000

Apply with systemctl daemon-reload and a restart of PgBouncer. A restart drops all server connections and clients reconnect at once, so plan it for a low-traffic window. Then size max_client_conn using the rule of thumb in reverse: with LimitNOFILE=21000, a max_client_conn of 10000 fits the fd_limit >= max_client_conn x 2 + 500 rule with room to spare.

On SysV init systems, set ulimit -n inside the init script or the service’s defaults file before the daemon starts. Changing the limit after the process is running has no effect; the FD ceiling is fixed at process start.

Lower max_client_conn to fit reality

If you cannot or do not want to raise the FD limit, bring max_client_conn down to what the FD budget supports and treat client-slot exhaustion as a capacity signal. A lower honest limit beats a higher fictional one: with an honest limit, used_clients / max_client_conn alerts mean something.

Fix the application side

If SHOW CLIENTS shows leaked connections, no limit change fixes the problem. Reduce application pool sizes, fix code paths that open connections without closing them, and account for the full fan-out: application pool size times instance count must fit inside max_client_conn with headroom. Load balancer health checks also consume client slots; include them in the budget.

Verify after every change

After any reload or restart, re-run checks 2, 4, and 5. Confirm /proc/<pid>/limits shows the new FD limit and SHOW CONFIG shows the intended max_client_conn. PgBouncer can keep old or lowered values, so “I changed the config” is not evidence of anything.

Prevention

  • Validate the pair together. Any change to max_client_conn must ship with a corresponding FD-limit change, verified on the running process via /proc/<pid>/limits.
  • Codify the formula. Put fd_limit >= max_client_conn x 2 + 500 in a comment next to the setting in your config management so the next person does not tune one side alone.
  • Alert on the ratio, not the wall. used_clients / max_client_conn > 80% sustained gives you runway. Waiting for refusal log lines means users already saw errors.
  • Alert on FD usage independently. FD count vs the process limit catches the case where server connections, not clients, consume the budget.
  • Account for multiplication. Multiple PgBouncer processes (so_reuseport) each get their own per-process FD limit, but application instances multiply client connections across all of them. Do the arithmetic per process and in aggregate.
  • Parse the log. Refusals and EMFILE exist only in the log. A monitoring setup that scrapes only SHOW output is blind to both.

How Netdata helps

  • Tracks used_clients against max_client_conn continuously, so you see the utilization ratio trending toward the refusal wall instead of discovering it from application errors.
  • Collects per-process file-descriptor usage from /proc, letting you correlate FD consumption with client and server connection counts on one timeline.
  • Correlates client-slot saturation with pool signals (cl_waiting, maxwait, sv_active) so you can tell “too many clients” apart from “pool exhaustion driving retries that create clients.”
  • Surfaces connection-count anomalies per database and user, which helps pinpoint whether growth is a leak (steady, one source) or a traffic event (broad, correlated).
  • Retains per-second history across restarts, making it visible when PgBouncer restarted with a lowered max_client_conn or a reset FD limit.