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
pgbouncerdatabase are exempt frommax_client_conn. You can always get in to runSHOWcommands even when clients are being refused. - PgBouncer may lower
max_client_connat 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
| Cause | What it looks like | First thing to check |
|---|---|---|
max_client_conn raised without raising the FD limit | Refusals start well below the configured limit, or the config was lowered at startup | SHOW CONFIG value vs grep "Max open files" /proc/<pid>/limits |
| Default ulimit (1024) never changed | PgBouncer refuses connections around a few hundred clients despite a four-digit config | /proc/<pid>/limits on the running process |
systemd unit missing LimitNOFILE | limits.conf was edited but the service still runs with a low limit | /proc/<pid>/limits, not ulimit -n in your shell |
| Application connection leak | used_clients climbs steadily toward the real limit under normal load | SHOW CLIENTS for connections with very old connect_time |
| Retry storm after an incident | used_clients spikes during pool exhaustion as applications reconnect | PgBouncer 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
Establish the three numbers. From the quick checks: the loaded
max_client_conn(fromSHOW CONFIG), the process FD limit (from/proc/<pid>/limits), and the current FD count. IfSHOW CONFIGreports a lowermax_client_connthan your ini file, PgBouncer lowered it at startup because the FD limit could not support your value.Compute the budget. Take the FD limit. Subtract total server connections (
used_serversplus headroom for pool growth up to the sum of allpool_sizevalues). Subtract roughly 500 for listening sockets, the log file, pipes, admin sockets, and DNS. What remains is your real client capacity. Compare it tomax_client_conn.Check whether FDs or config is the binding constraint. If the open FD count is near the FD limit while
used_clientsis well belowmax_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.If the limit is real and being reached legitimately, find the consumers. Run
SHOW CLIENTSand group by source address andconnect_time. Connections with very oldconnect_timeand no recent activity point to an application leak. A broad distribution of fresh connections points to genuine concurrency growth or a retry storm.Check for a retry cascade. If refusals coincide with
cl_waiting > 0and risingmaxwaitinSHOW 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
| Signal | Why it matters | Warning sign |
|---|---|---|
used_clients / max_client_conn | Proximity to the hard refusal wall | >80% sustained; >95% is urgent |
Open FD count vs /proc/<pid>/limits | The real ceiling, which may be lower than the config | FD usage >80% of limit |
PgBouncer log: no more connections allowed | Active refusal. Only visible in logs, not in any SHOW command | Any sustained rate |
PgBouncer log: Too many open files | FD exhaustion, risk of crash-loop and lost log FD | Any occurrence |
used_clients trend over weeks | Capacity runway for client slots | Steady climb without traffic explanation |
SHOW CLIENTS old connect_time entries | Leaked connections consuming slots indefinitely | Long-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_connmust 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 + 500in 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
SHOWoutput is blind to both.
How Netdata helps
- Tracks
used_clientsagainstmax_client_conncontinuously, 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_connor a reset FD limit.
Related guides
- PgBouncer capacity planning: runway for pools, clients, and PostgreSQL slots
- PgBouncer pool exhaustion: clients queue, wait times climb, and the retry cascade
- PgBouncer monitoring checklist: the signals every connection pooler needs
- How PgBouncer actually works in production: a mental model for operators
- PgBouncer pool utilization high: sv_active approaching pool_size before clients queue
- PgBouncer maxwait high: the oldest client waiter and how close it is to timing out
- PgBouncer avg_wait_time high: the latency the pool itself is injecting
- PgBouncer query_wait_timeout: clients disconnected after waiting too long for a connection
- PgBouncer monitoring maturity model: from survival to expert
- PgBouncer advisory locks in transaction mode: orphaned locks and mysterious contention
- PgBouncer LISTEN/NOTIFY not working: why pub/sub needs session pooling
- PgBouncer prepared statement does not exist: transaction pooling and lost session state






