You deploy at 2am, everything goes green, then ten minutes later the app logs fill with FATAL: sorry, too many clients already and every request 500s. Classic trigger: a Java or Node service with a default pool of 10, scaled to 20 pods, all hitting one Postgres instance with max_connections = 100. That's 200 potential connections against a limit of 100. Postgres hits the ceiling, refuses new ones, and your health checks start failing. I've seen this exact pattern at three different companies. The math never gets done until production breaks.
What the error actually means
Postgres has a hard cap called max_connections. Every client — app server, admin, psql session, monitoring agent — takes one slot. When max_connections is reached, new connections get rejected with SQLSTATE 53300 and that FATAL message. Existing connections keep working, which is why the app feels half-broken instead of fully dead.
Two things make this worse than it looks. First, Postgres forks a backend process per connection — around 5-10MB of RAM each. So cranking max_connections to 1000 isn't free; you'll swap or OOM. Second, if your app leaks connections (opens them, never closes on error paths), the count creeps up over hours until you're cooked.
Check the current state first
Don't guess. Run these before touching anything.
-- How close are you to the ceiling?
SELECT count(*) AS used,
current_setting('max_connections')::int AS max
FROM pg_stat_activity;
-- Who's holding connections?
SELECT usename, application_name, state, count(*)
FROM pg_stat_activity
GROUP BY 1,2,3
ORDER BY 4 DESC;
-- The real killer: idle in transaction
SELECT pid, usename, state, query_start, now() - state_change AS idle_time
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY idle_time DESC;
If idle in transaction shows up with long times, that's your bug. Someone opened a transaction and walked away — usually a Java app missing a commit() or a Python service with autocommit off.
The fix, in order
1. Kill the offenders (temporarily)
Get breathing room so the app can recover. Only kill idle ones — don't nuke an active query mid-write.
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND state_change < now() - interval '5 minutes';
2. Raise max_connections — carefully
Bump it, but understand the cost. Rule of thumb: each connection uses ~10MB. On a 16GB box you can safely go to 200-300. Beyond that, add a pooler.
-- Check current value
SHOW max_connections;
-- postgresql.conf
max_connections = 200
-- Reload without restart
SELECT pg_reload_conf();
Also raise shared_buffers if you can — a bigger connection count without more buffers just means more contention on the same cache.
3. Fix the connection leak in your app
This is the real fix. The pool bump is a band-aid. Check your code for:
- Connections opened outside a try/finally or context manager
- Missing
conn.close()on exception paths - HikariCP
leakDetectionThresholdnot set — turn it on, set to 60 seconds, and read the logs - Django:
CONN_MAX_AGEset to something huge with noCONN_HEALTH_CHECKS
For HikariCP, this config exposes leaks fast:
leakDetectionThreshold=60000
maximumPoolSize=10
minimumIdle=2
connectionTimeout=5000
4. Put PgBouncer in front
If you're running microservices, you need a pooler. Period. PgBouncer in transaction mode lets 500 app connections share 50 real Postgres backends. That's the whole trick.
; pgbouncer.ini
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
reserve_pool_size = 5
server_reset_query = DISCARD ALL
Use transaction mode unless your app uses prepared statements or session-level state — then use session mode and add more PgBouncer instances instead.
5. Reserve slots for the superuser
Always keep a few connections for yourself. Otherwise you can't even log in to fix the problem.
superuser_reserved_connections = 5
If it's still failing
- Check
pg_stat_activityagain — did the count actually drop, or did something reconnect instantly? - Look at the OS limits.
ulimit -non the postgres user, and kernelsomaxconn. Sometimes you hit the OS before Postgres. - Test with psql from the DB host itself. If local works but remote doesn't, it's
pg_hba.conforlisten_addresses, not connection count. - Check max_worker_processes and max_parallel_workers. Parallel query workers eat into the same process budget.
- Look at cron jobs and monitoring. That Prometheus exporter polling every 5 seconds holds a connection. Ten of them add up.
If you're bumping max_connections more than once a quarter, you've got a leak or you need PgBouncer. Postgres scales up, not out — respect that and your database will stop biting you.
Grab the current docs on connection settings before you tune anything blind. And test changes on staging with real load — a quiet dev box tells you nothing about connection pressure.