[{"data":1,"prerenderedAt":6},["ShallowReactive",2],{"post-content-postgres-too-many-clients-already-connection-math":3},{"content":4,"lastModified":5},"\u003Cfigure>\n  \u003Cimg src=\"https:\u002F\u002Fimages.pexels.com\u002Fphotos\u002F1148820\u002Fpexels-photo-1148820.jpeg?auto=compress&cs=tinysrgb&w=1200\" alt=\"Dozens of network cables plugged into a switch with only so many ports\" loading=\"lazy\" \u002F>\n\u003C\u002Ffigure>\n\n\u003Cp>Last Monday a service started throwing \u003Ccode>FATAL: sorry, too many clients already\u003C\u002Fcode> about forty times in a row, then went quiet. CPU on the Postgres box sat at 9%. Nothing had deployed that morning. The database was healthy and refusing to talk to us, which is my least favorite combination.\u003C\u002Fp>\n\n\u003Cp>I wrote about \u003Ca href=\"\u002Fposts\u002Fmysql-too-many-connections-pool-math\u002F\">MySQL's version of this problem\u003C\u002Fa> a couple of weeks ago. Postgres fails the same way in production, but the details are different, and the details are what bite you.\u003C\u002Fp>\n\n\u003Ch2>Every connection is a process\u003C\u002Fh2>\n\n\u003Cp>MySQL keeps connections as threads inside one process. Postgres forks a full operating system process for every connection. Each one holds a few megabytes at rest, and more the moment it runs a sort or a hash join with its own \u003Ccode>work_mem\u003C\u002Fcode>. Past a few hundred active backends, the CPUs spend their time context-switching and fighting over locks instead of running your queries.\u003C\u002Fp>\n\n\u003Cp>So a 16-core server can genuinely run about 16 queries at once. Giving it 800 connections does not give you 800 concurrent queries. It gives you 800 processes standing in line inside the kernel.\u003C\u002Fp>\n\n\u003Cp>That's why the first instinct, raising \u003Ccode>max_connections\u003C\u002Fcode>, usually makes things worse. The default of 100 looks small until you understand what each slot costs. And if you do change it, note that it needs a restart, not just a reload. I learned that during the incident instead of before it.\u003C\u002Fp>\n\n\u003Ch2>Find out who is holding the slots\u003C\u002Fh2>\n\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT usename, application_name, client_addr, state, count(*)\nFROM pg_stat_activity\nWHERE backend_type = 'client backend'\nGROUP BY 1, 2, 3, 4\nORDER BY count(*) DESC;\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>In our case one \u003Ccode>application_name\u003C\u002Fcode> owned most of the 100 slots, nearly all in state \u003Ccode>idle\u003C\u002Fcode>. The app had opened them and never let go. That's the whole diagnosis in one query: you don't need more connections, you need fewer idle ones.\u003C\u002Fp>\n\n\u003Ch2>The multiplication\u003C\u002Fh2>\n\n\u003Cp>Most frameworks keep a pool per process, and the totals multiply quietly. Twelve pods, four worker processes each, a pool of 20 per worker. That's 960 connections against a \u003Ccode>max_connections\u003C\u002Fcode> of 100, and nobody ever wrote those three numbers side by side. Every autoscaling event and every cron worker makes it worse. Serverless is the worst of all, since each concurrent invocation can want its own connection.\u003C\u002Fp>\n\n\u003Cp>Do the multiplication on paper before you touch a single setting. It takes two minutes and it ends the argument.\u003C\u002Fp>\n\n\u003Ch2>Put PgBouncer in front\u003C\u002Fh2>\n\n\u003Cp>The real fix is a pooler. PgBouncer takes thousands of cheap client connections and funnels them through a small set of real Postgres backends:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-ini\">; pgbouncer.ini\n[databases]\napp = host=10.0.0.5 port=5432 dbname=app\n\n[pgbouncer]\nlisten_port = 6432\npool_mode = transaction\nmax_client_conn = 5000\ndefault_pool_size = 30\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>With \u003Ccode>pool_mode = transaction\u003C\u002Fcode>, a server connection is lent to a client for the length of one transaction, then handed back. Five thousand app connections can share thirty real backends. Point your app at port 6432 and the pressure disappears.\u003C\u002Fp>\n\n\u003Cp>There is one honest catch. In transaction mode, consecutive transactions from the same client may run on different backends, so anything that depends on session state breaks. That means \u003Ccode>SET\u003C\u002Fcode> without \u003Ccode>LOCAL\u003C\u002Fcode>, session-level advisory locks, \u003Ccode>LISTEN\u002FNOTIFY\u003C\u002Fcode>, and temp tables that outlive a transaction. Check whether your app actually uses any of those before you flip the mode. Prepared statements used to be on that list too, but PgBouncer 1.21 and newer support them through \u003Ccode>max_prepared_statements\u003C\u002Fcode>, and most modern drivers work fine now.\u003C\u002Fp>\n\n\u003Cp>If you're on a managed platform, check what it ships before installing anything. Supabase has Supavisor, Neon has its own pooler, RDS Proxy covers AWS. Half the incidents I read about online are people running direct connections on a platform that already gave them a pooler URL.\u003C\u002Fp>\n\n\u003Ch2>Size it, then set the timeouts\u003C\u002Fh2>\n\n\u003Cp>For the server-side pool, a decent starting point is \u003Ccode>(cores x 2) + effective disk spindles\u003C\u002Fcode>. On a 16-core box with SSDs, that's roughly 30 to 40 active connections. Benchmark from there instead of guessing higher. Keep \u003Ccode>max_connections\u003C\u002Fcode> a bit above the pooler's total and leave \u003Ccode>superuser_reserved_connections\u003C\u002Fcode> alone so you can still log in to fix things when it all goes wrong. Nothing is funnier, in a grim way, than an exhausted server you can't even \u003Ccode>psql\u003C\u002Fcode> into.\u003C\u002Fp>\n\n\u003Cp>Then close the leak at the source with timeouts:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-sql\">ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';\nALTER SYSTEM SET idle_session_timeout = '15min';  -- PostgreSQL 14+\nSELECT pg_reload_conf();\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>The first one has existed since 9.6 and kills exactly the sessions that cause lock pileups. Be careful with \u003Ccode>idle_session_timeout\u003C\u002Fcode> if clients hold long-lived pools directly; their drivers need to handle the reconnect.\u003C\u002Fp>\n\n\u003Cp>The MySQL post ended with the same lesson and I'll keep repeating it until it sticks: count before you scale. I keep the \u003Ccode>pg_stat_activity\u003C\u002Fcode> query and my pgbouncer.ini template saved in \u003Ca href=\"\u002Fsnippetark\u002F\">Snippet Ark\u003C\u002Fa> now, so the next incident starts with a paste instead of a frantic search.\u003C\u002Fp>\n","2026-10-26",1793106023367]