Postgres connection pooling, not a read replica, is the right fix when your log fills with 'FATAL: sorry, too many clients already' while CPU sits idle. Postgres forks a full operating system process per connection, and a pooler multiplexes thousands of lightweight client sockets onto a small pool of real backend processes. This article explains the architecture, the failure mode, and the diagnosis.
Why Postgres Runs Out of Connections While CPU Sits Idle
Postgres rejects new clients with 'FATAL: sorry, too many clients already' because it ships with a default limit of 100 connections, and each connection is a full operating system process rather than a thread. When the slots are gone, the database refuses work even if CPU and memory are idle, which is why monitoring dashboards stay green while the application throws errors.
The PostgreSQL manual states the design in one sentence: to handle multiple concurrent connections, the server forks a new process for each one. That process has its own memory space and file handles and lives until the client disconnects. The trade is deliberate. If one backend seg faults, only that session dies; the crash cannot spread to other clients on the same machine. The front door is expensive, but a failure is contained.
Two facts matter before you spend money. First, 100 is a default, not a law. You can raise it, at the cost of a restart, and on some managed platforms you cannot touch it at all. Supabase, the Postgres backend platform, gives its small tier 90 direct connections and caps its largest tiers at 500 no matter how large the machine. Second, the limit only hurt because of an unstated assumption: connections were assumed to be long-lived, opened once by a fixed fleet of app servers and held for weeks. That assumption broke when the execution model underneath changed.
What One Postgres Connection Actually Costs in Memory
A Postgres connection costs roughly 9.5 MB of memory per the formulas vendors hard-code into their connection limits, even though the true incremental cost of an idle backend is much lower. The gap between the two numbers is where most confusion about Postgres connection pooling starts.
Postgres committer Andres Freund measured the real figures: an idle backend shows about 17 MB of memory, but most of that is shared between processes. The true incremental cost is closer to 7.5 MB, dropping to about 1.3 MB with huge pages enabled. The floor is small. What vendors budget is not.
Two independent vendors landed on nearly the same number without coordinating publicly. Amazon RDS sets its connection limit by dividing instance memory by 9,531,000 bytes. Neon, a separate serverless Postgres company, gives a 4 GB instance 419 connections, which divides out to roughly the same 9.5 MB. Neither published a post declaring the process model unscalable. They wrote the judgment into a default, which is a more reliable signal than any announcement: watch what infrastructure companies configure, not what they say.
Why Serverless and Lambda Detonate the Connection Limit
Serverless functions break the long-lived connection assumption because they boot on a request, do one thing, and die. Every execution environment that wakes up opens its own connection instead of sharing a pool. Postgres has no way to know these clients belong to the same application, so it forks a fresh backend process for each one and tears it down seconds later, which is the most expensive thing the database does, repeated once per request.
The collision fits in one line of arithmetic. If your Lambda concurrency is 3,000 and your max_connections is 500, then 2,500 requests hit the rejection error in the same second. A traffic spike does not add connections gradually; it detonates them, with hundreds of processes forking and requesting kernel memory on a machine that was idle a moment earlier.
Web servers hit this exact wall first. Apache forked a process per request until traffic made it untenable, and Nginx won by refusing to give a connection its own thread or process. Postgres is Apache in this story. The event-loop rework has not been merged, which is why the failure shows up as refused connections while every utilization graph looks healthy.
Why a Read Replica Does Not Fix Connection Errors
A read replica does not fix connection-limit errors because a hot standby must run with max_connections set at or above the primary's value, so it inherits the same ceiling instead of adding pool capacity. You pay for a second machine and buy the same limit twice, and if the connections drowning you are writes or transactions that touch a write, the replica cannot take a single one.
The bill is straightforward on RDS: a read replica is built as a standard instance at the same rate as its class, so matching your primary size takes the database bill to 200% with no copy discount. Meanwhile the error keeps firing, because the doorway did not get wider.
A replica is still a good product for the problem it actually solves. Heavy reads, slow queries, and a primary pinned at high CPU are a work problem, and a replica moves work. Idle CPU with refused connections is a doorway problem, and only a pooler addresses it. A replica is the correct tool routinely sold as the answer to a question it does not answer.
Diagnose Doorway vs Work Problems with pg_stat_activity
One query tells you which problem you have before you spend anything: select state, count(*) from pg_stat_activity group by state. It separates sessions doing work from sessions holding a forked process open while doing nothing.
Crunchy Data's connection tuning guide shows a real example of the doorway problem: seven active sessions, 69 idle, 26 idle in transaction, and 11 idle and aborted. Seven sessions were doing actual work while 106 held forked operating system processes open to do nothing at all. That database was never busy. It was occupied.
Read the result as a decision rule. If active counts are low and idle or idle-in-transaction counts are high while CPU sits under roughly 30%, you have a connection problem and postgres connection pooling is the fix. If active sessions dominate and CPU is pegged, you have a workload problem and a replica is the right buy; a pooler on its own would make it worse by letting more work through the door.
How PgBouncer and RDS Proxy Multiplex Connections
A connection pooler sits in front of Postgres and speaks the same wire protocol on both sides, so your app thinks it is talking to Postgres and Postgres thinks it is talking to one very well-behaved client. In between, it hands a real backend connection to whichever client needs it right now and takes it back the moment the transaction finishes.
The weight difference is the whole argument. A Postgres backend is a forked process with a 9.5 MB vendor budget attached; a client inside PgBouncer, the lightweight single-threaded C pooler, costs roughly 2 KB, several thousand times lighter for a connection that spends most of its life idle. Supabase own figures for its Supavisor pooler claim 90 direct connections versus 400 pooled on a small instance, 500 direct versus 12,000 pooled at the top of the range, and a stress test of just over a million simultaneous client connections riding on a pool of 400 real database connections. Those are vendor-reported numbers from the vendor selling the pooler, not independent measurements.
The managed version of the same idea is Amazon RDS Proxy, which the video prices at about $22 a month against a second full-price instance. Transaction pooling is where the savings come from, and it has a real cost: a client does not get the same backend twice in a row, so session state breaks. SET and RESET, LISTEN/NOTIFY, cursors held across transactions, session-level advisory locks, and session-level PREPARE/DEALLOCATE are all unavailable, and PgBouncer documents the list in its feature matrix so nobody can claim they were not told.
Prepared Statements Under Transaction Pooling: The Myth That Will Not Die
Half the posts you will read say transaction pooling breaks prepared statements. That objection stopped being true in October 2023, and in January 2025 protocol-level named prepared statement support became PgBouncer's default in version 1.22 and later per its release notes. The most repeated objection to pooling has been fixed for years and is still being copied into new architecture decisions.
This matters because compatibility language should match documented reality. PgBouncer is a drop-in endpoint on the wire protocol, but transaction mode changes session semantics, and its own docs say so. Read the feature table before you flip the mode, especially if your ORM or driver relies on session state. Prepared statements, the most common worry, are no longer on the broken list.
Postgres 18 Async I/O and the Threaded Future
Postgres 18 shipped an asynchronous I/O subsystem on September 25, 2025, the biggest change to how Postgres reads from disk in years. Sequential scans, bitmap heap scans, and vacuum can now queue reads instead of waiting on each one in turn, and independent benchmarks put cold sequential reads two to three times faster on fast local disks, per PostgreSQL 18 coverage. The caveat: PlanetScale tested 17 against 18 and found that on network-attached storage the plain default could still win, so there is no single best setting.
The front door did not move. Search the release notes for the word pooler and it is not there; max_connections did not change. Postgres 19 is in beta with general availability expected in autumn 2026, with planned items like native repack and parallel autovacuum and no built-in pooler.
The engine did partially fix the worst symptom once. Snapshot scalability rework in Postgres 14 took a server carrying 10,000 idle connections from 370,000 to 1.1 million transactions per second, per the PostgreSQL scalability work tracked by Andres Freund. That fixed how snapshots are computed, not the process model. Freund's own conclusion stands: these improvements do not address all connection scalability issues, the real fix is threads instead of processes, and that work targets Postgres 20 at the earliest.
FAQ: Postgres Connection Pooling in Practice
- Do I need a pooler or a read replica? Run select state, count(*) from pg_stat_activity group by state. Low active counts with many idle sessions and CPU under about 30% means a connection problem; buy a pooler. High active counts with pegged CPU means a workload problem; a replica is the right purchase.
- How much does connection pooling cost? A managed proxy such as Amazon RDS Proxy runs about $22 a month on a small setup, per the video's pricing. PgBouncer is free open-source software you can self-host, and Supabase and Neon bundle a pooler with their platforms.
- Does transaction pooling break prepared statements? No. PgBouncer gained protocol-level named prepared statement support in October 2023 and made it the default in January 2025. Session-level features like advisory locks, LISTEN/NOTIFY, and cross-transaction cursors do remain unavailable in transaction mode.
- Why did my read replica not fix 'too many clients'? A hot standby must run max_connections at or above the primary's value, so it inherits the ceiling instead of adding pool capacity. You paid for a second machine and got the same limit twice.
- Will Postgres fix this natively? Postgres 14 eased the idle-connection penalty and Postgres 18 overhauled I/O, but neither changed the process-per-connection model. Threaded backends targeting Postgres 20 at the earliest are the real fix, per Andres Freund.
From a Video Explanation to an Article You Can Ship
The core lesson here is that a costly scaling decision often hides behind a default nobody read, and the clearest explanations of those defaults usually live in videos, talks, and walkthroughs rather than documentation. If you have that kind of knowledge trapped in a recording, a blog post makes it searchable and quotable.
That is the workflow at Skala Blog: paste a YouTube URL, get a transcription, and turn it into a structured written article your team and your readers can actually find. The diagnosis in this piece took a 12-minute video to explain; written down, it takes a reader ten seconds to check.
Fork this article
Start a new branch from the same video, shaped your way. You keep the credit; the original keeps the attribution.
A fork in another language is filed as a translation of this article, so the two pages point at each other. You can unlink it later from the editor.
0/240
You are creating
- Format
- For
- Language
- Source
- Your angle
You will be asked to sign in before it is generated.
Buy credits