Enjoying this issue?
Get tomorrow's AI & engineering digest in your inbox — hand-picked, summarized, and always spam-free.
TLDR
The $22 fix is a connection pooler like PgBouncer, which solves the "too many clients already" error by multiplexing thousands of client connections onto a handful of real Postgres processes. The root cause is Postgres's process-per-connection model, which works fine for long-lived app servers but collapses under serverless functions that open and close connections per request. The common knee-jerk fix of adding a read replica doesn't help because the replica inherits the same connection limit, and it often costs ten times more than the pooler.
Key points
- Postgres forks a new OS process per connection, each costing roughly 9.5 MB of budget on managed services like RDS, and the default cap is 100 connections.
- Serverless functions (e.g., Lambda) open a new connection per invocation, not a shared pool, so a traffic spike can exhaust the 100 slots instantly even when CPU is idle.
- Adding a read replica does not raise the connection ceiling because the replica must have max_connections set at least as high as the primary, and it cannot serve write connections.
- The real fix is a connection pooler that sits in front of Postgres and lets 10,000 clients share 20 actual database connections, with each pooler client costing about 2 KB instead of 9.5 MB.
- Transaction-mode pooling (the default in PgBouncer) breaks session-level features like LISTEN/NOTIFY and cursors across transactions, but prepared statements have worked since October 2023.
- The first diagnostic step is to check pg_stat_activity: if you see many idle connections and low CPU, you have a doorway problem, not a work problem, and a pooler is the right fix.
- Postgres 18 shipped a major async I/O rewrite that speeds up disk reads, but it did not touch the connection model at all, and Postgres 19 (due September 2025) still has no built-in pooler.
- Threads instead of processes are planned for Postgres 20 at the earliest, so the pooler remains the pragmatic solution for years to come.
Tools mentioned
Techniques
- connection pooling
- transaction mode pooling
- diagnosing connection issues with pg_stat_activity
- adding read replicas for work problems (high CPU, slow reads)
- async I/O configuration in Postgres 18
Stop scrolling. Start reading smarter.
Receive the day's most important AI & engineering updates in one concise email. No spam.
Transcript (captions)
3 in the morning. Your checkout page is throwing 500s and your dashboard is solid green. CPU 8%. Memory comfortable. Disc bored. Nothing you are monitoring says anything is wrong. Then you open
the Postgress log itself. One line repeated 40,000 times over the last 9 minutes. Fatal. Sorry, too many clients already. That is the entire error and it is also the entire diagnosis. Your
database is not short of CPU and not short of memory. it has run out of something else entirely. It has run out of places to put a connection. By default, Postgress ships with exactly
100 of them. So, you do what the console's blue button suggests and what the internet suggests. You add a read replica. Your build doubles overnight. The error keeps firing because the new
replica has the same 100 slots. The replica is doing precisely what the manual says a standby does. That line is in the docs. People skip it. The fix you actually needed is a singlethreaded C
program. and the managed version of it runs about $22 a month. So where does that 100 come from? Why does serverless walk straight into it and which metric tells you which fix you need? Because
Postgress just shipped the biggest rewrite of its IO engine in years and it did not touch this at all. Open the Postgress manual at chapter 1 and it tells you in a single sentence to handle
multiple concurrent connections. quote it starts forks a new process for each connection not a thread not a corine an operating system process. So when your app opens a connection the supervisor
process forks a whole new copy of the database server just for you its own memory space its own file handles its own everything. It lives until you disconnect. Here is the analogy that
fits. Most databases are a call center. One floor, one pool of agents. Whoever is free takes your call. Postgress builds you a private office the moment you dial and keeps it standing until you
hang up which sounds absurd until you ask why. If your query hits a bug in seg faults, it takes down your office. One process dies. The other sessions on that machine carry on as if nothing happened.
That is the trade Postgress made an expensive front door in exchange for a crash that cannot spread. It has been a good trade for most of this database's life and a large part of why it has a
reputation for not losing your data. The design is not wrong. The problem is what happens when you multiply it. Every office costs memory and the number people quote is 5 to 10 megabytes per
connection. That figure is doing a lot of work because it is really two numbers. Andre Frod a Postgress committer measured it properly. An idle backend shows about 17 megabytes which
is misleading because most of that is shared. The true incremental cost is closer to 7 and 12 and with huge pages on 1.3. So the floor is small, but look at what the company selling you
Postgress actually budget for. Amazon's RDS sets your connection limit by taking your instance memory and dividing it by 9,531,000 bytes. That is 9 1/.5 megabytes per
connection hardcoded into a formula. Neon, a completely different company, gives a 4 GB instance, 419 connections divided out 9 1/.5 megabytes. Two independent vendors landing on the same
budget within a fraction of a percent. Neither published a blog post saying the process model does not scale. They wrote it into a default. Watch what infrastructure companies configure, not
what they announce. One thing to be precise about, 100 is a default, not a law. You can raise it at the cost of a restart. And on managed Postgress, you often cannot touch it. Superbase gives
it small tier 90 connections and caps its largest tiers at 500, however big the machine gets. For most of the time this design has existed, a hundred was plenty because of an assumption that
went unstated. Connections are longived. You ran a fixed fleet of app servers, each one opening a pool at boot and holding it for weeks. 20 servers, 20 connections each, 400 in total, and that
was your entire company. The front door was slow, but you walked through it once. Then the execution model changed underneath it, and the assumption went unexamined. Serverless functions do not
boot once and live for weeks. They boot on a request, do one thing and die. And every execution environment that wakes up opens its own connection, not a shared pool, its own. Postgress has no
idea these are the same application. So it forks a fresh process for each one, then tears it down again seconds later. The most expensive thing your database does on repeat once per request. Here is
the collision in one line of arithmetic. Your Lambda concurrency is 3,000. Your max connections is 500. 25,500 requests. get that three in the morning error in the same second and it does not arrive
gradually. A traffic spike does not add connections. It detonates them. Hundreds open in seconds, each forking a process, each asking the kernel for memory on a machine that was idle a moment earlier.
This is a design built for longived server processes meeting an execution model built on the opposite assumption. Neither side is wrong. They were built for different decades. Web servers hit
this exact wall first. Apache forked a process per request until the traffic made it untenable and EngineX won by refusing to give a connection its own thread. Postgress is Apache in this
story. The event loop has not been merged yet. So your site is down and the graph everyone is staring at says database. The instinct is to scale the database and to most teams scaling a
database means one thing. Add a replica receipt one. On RDS a read replica is built as a standard database instance at the same rate as its instance class. Same size as the primary means your
database bill just went to 200%. There is no copy discount. Receipt two, and this is the one that stings. A hot standby must run with max connections set at or above the primary value, so
the replica does not hand you an independent pool of slots. It inherits the ceiling and it is obliged to match it. You paid for a second machine and bought the same limit twice. Receipt
three. Your rights still go to one place. If the connections drowning you are rights or transactions that touch a right, the replica cannot take a single one of them. Now, in fairness, a read
replica is a good product and there is a version of the story where it is exactly right. Heavy reads, slow queries, a primary pinned at 100% CPU. Buy the replica. It is the correct tool sold as
the answer to a question it does not answer, which gives you the split and it takes 10 seconds to check. Pegged CPU, slow queries, high load. You have a work problem and a replica moves work. Idle
CPU connections refused. You have a doorway problem and a replica gives you a second doorway exactly as narrow as the first. One query settles it. Select count state from PG stat activity group
by state. Here is a real example from Crunchy Data's own guide. Seven active, 69 idle, 26 idle and transaction, 11 idle and aborted. Seven sessions were doing work. 6 were holding a forked
operating system process open in order to do nothing at all. That database was never busy. It was occupied. So before you buy the second machine, answer this first. Is your CPU pegged or is it
bored? Both fixes cost real money and only one of them is aimed at the thing that is actually failing. What a pooler does is almost embarrassingly simple, which is part of why it gets skipped. It
sits in front of Postgress and speaks the same wire protocol on both sides. Your app thinks it is talking to Postgress. Postgress thinks it is talking to one very well- behaved
client. In between, it does the thing your database cannot. It hands one real connection to whoever needs it right now and takes it straight back the moment they are done. 10,000 clients, 20 actual
processes. The weight difference is the whole argument. A Postgress backend is a forked process with a 9 12 megabyte budget attached. A client inside PG Bouncer is about 2 kilobyt, roughly
4,000 times lighter for the part of a connection that spends most of its life doing nothing. The production numbers back that up. Superbas's small instance, 90 direct connections, 400 through the
pooler on identical hardware. At the top of their range, 500 direct against 12,000 poolled, same machine. Superbase benchmarked its own pooler holding just over a million simultaneous client
connections against a pool of 400 connections to the actual database. 2 and a half thousand clients riding on each Postgress process. But a pooler is not free and anybody selling it to you
as free has not operated one. Transaction mode is where the savings come from. And transaction mode means a client does not get the same backend twice in a row. So anything that lives
in a session breaks at and reset unavailable. Listen unavailable. Cursors held across transactions. Session level advisory locks. A school level prepare and deallocate unavailable. PG bouncer
prints them in a table so you cannot claim you were not told. There is one item on that list the internet is still wrong about. Half the posts you will read say transaction pooling breaks
prepared statements. That stopped being true in October 2023 and in January 2025 it became the default. So the most repeated objection to pooling has been fixed for nearly 3 years and is still
being copied into new architecture decisions. The real bill meanwhile is small. Amazon's manage proxy runs about $22 a month against a second instance at full price. Which brings us to the
strangest part of this story. On the 25th of September 2025, Postgress 18 shipped an asynchronous IO subsystem. The biggest change to how Postgress reads from disk in years. Sequential
scans, bitmap heap scans in vacuum can now cue their reads instead of waiting on each one in turn. Independent benchmarks put cold sequential reads two to three times faster on fast local
discs. Though there is a caveat worth keeping. Planet Scale tested 17 against 18 and found the fancy option is not automatically the winner on network attached storage. The plain default beat
it. There is no single best setting. Now search those same release notes for the word pooler. It is not there. Search for a change to max connections. There is not one. The engine got dramatically
faster at reading and the front door did not move a millimeter. Postgress 19 is already in beta with the second beta landing in July and general availability expected this September or October.
Planned advice, native repack, parallel autovacuum, property graph queries, no built-in pooler. To be fair, they fixed the worst version of this once. Froin snapshot rework in Postgress 14 took a
server carrying 10,000 idle connections from 370,000 transactions a second up to 1.1 million. The idle penalty was essentially erased. But that fixed how snapshots are computed. It did not touch
the process. His own conclusion in his own words, these improvements do not address all connection scalability issues in Postgress. The real fix is threads instead of processes and that
work targets Postgress 20 at the earliest. There is one more door out of this room and it deserves naming. A meaningful share of the applications reaching for a pooler or a replica are
not large. They are one modestlysized box away from needing neither. Light stream streams is SQLite's changes to object storage continuously. And in October 2025, it added point in time
recovery from about a dozen files. For an app that fits on one machine, the connection ceiling stops existing because there are no connections. The scaling conversation usually starts one
step later than it should. So, the verdict, if your log says too many clients already while your CPU sits under 30%, the pooler wins and it is not close. $22 a month against a doubled
instance bill for a failure the replica cannot fix because it inherits the ceiling you are trying to escape the case it does not win pegged CPU and slow reads that is a work problem the replica
is the right buy and a pooler on its own makes it worse by letting more work through the door know which one you have before you spend I would still take the pooler at three times the price but read
the feature table before you flip the mode and a bet you can hold me to Postgress 19 goes generally available this autumn with no built-in pooler and 20 will not ship one either because the
people who could build it are building threads instead which leaves the question I actually want answered. The major managed providers all ship a pooler in front of Postgress as standard
now. So, at what point does the patch stop being a workaround and start being the missing feature?