Postgres Hit the Connection Limit. Here's How to Fix It for Good

Diagram of many app clients funneling through PgBouncer into a few Postgres connections, avoiding the connection limit

The Postgres connection limit is a hard cap on how many clients can be connected to your database at the same time. Cross it, and every new request gets one blunt reply: FATAL: sorry, too many clients already. No queue. No retry. Just a closed door.

This isn't a bug. Postgres builds in this wall on purpose. Once you understand why it exists, the fix becomes obvious, and it isn't the one most people reach for first. Below, we go from the symptom to the cause to the fix: why connections are expensive, how apps burn through them, how to find leaks, and how pooling keeps you online when traffic spikes.

TL;DR
  • Postgres allows 100 connections by default, shared by every app, worker and tool.
  • Each connection is a full operating system process, so idle connections still cost memory.
  • Raising max_connections usually delays the crash and adds contention.
  • Use pg_stat_activity to find idle leaks, then release connections in finally blocks.
  • Pool connections with PgBouncer and keep your app's own pools small.
  1. 0:00Intro
  2. 0:26Your app goes dark at peak
  3. 1:25Postgres allows just 100 by default
  4. 2:09Every connection is a whole process
  5. 3:05Three ways to burn through connections
  6. 3:56Launch day math that breaks Postgres
  7. 4:43Just set it to 10,000? Stop.
  8. 5:28More connections rarely means more speed
  9. 6:19Find idle connections with pg_stat_activity
  10. 7:13Always close what you open
  11. 8:04Connection pooling with PgBouncer
  12. 9:01One query's trip through the pool
  13. 9:53Right-size your app's pool
  14. 10:45Mistakes that bring back the error
  15. 11:36The whole fix in five lines
  16. 12:10Don't raise the limit. Pool instead.

Why does Postgres say 'too many clients already'?

Picture it. You launch a feature, traffic jumps, and suddenly every page throws an error. Your phone starts buzzing. The strange part? The database is perfectly healthy. It just won't let anyone in.

The message means exactly what it says. Every seat is taken. Postgres doesn't hold new connections in a queue or wait politely for a slot to open. It rejects them on the spot.

Because nearly every request in a typical app touches the database, the damage isn't limited to one page. Logins, checkouts, search and admin screens fail together, in the same second. And this error almost never appears in testing. It waits for real users and real traffic, which means it hits during a sale or a big launch, exactly when people care most about your product.

  • Everything that needs the database fails at once
  • It shows up under real load, rarely in testing
  • Downtime during a spike means lost sales and angry users

What is max_connections, and why is it only 100?

Postgres caps simultaneous connections with a setting called max_connections. Out of the box, it's 100. Think of a restaurant with a fixed number of chairs. When every chair is full, the next guest isn't seated a little later. They're turned away at the door.

That 100 is the total for everyone. Every app server, every background worker, every script and every person poking around with a database tool shares the same budget. A few slots are also reserved: by default, three are held back for superusers so an admin can still log in during a crisis. Your app gets a little less than 100. The budget is tight.

  • Default max_connections: 100
  • Reserved for superusers by default: 3
  • Shared across apps, workers, scripts and admin tools

Why every Postgres connection is so expensive

Here's the detail that explains everything. A Postgres connection is not a lightweight thread. When a client connects, the main listener, called the postmaster, forks a brand new backend process whose only job is serving that one client. One client, one process, for as long as the connection stays open.

Each of those processes carries its own memory, caches and state. And an idle connection still holds that memory. Sitting there doing nothing is not free.

So the limit isn't Postgres being stingy. It's a safety rail. Without it, a flood of clients could spawn so many processes that the whole server grinds to a halt. The cap protects the machine from drowning in work it can't afford.

How apps run out of Postgres connections

If 100 sounds like plenty, look at how quickly it disappears. There are three usual suspects, and outages often involve two at once. The first is leaky code: a function opens a connection, hits an error, and never gives it back. Each leak is small. They pile up quietly until you hit the wall.

The second is serverless. Every copy of your function can open its own connection, and when traffic rises the platform launches more copies, each grabbing another seat. The third is a plain traffic spike. More users means more app servers, each with its own set of connections. Your code is fine. There's just a lot more of it.

Now the math. A small online shop runs a flash sale. Autoscaling jumps from four app servers to ten. Each server keeps a pool of ten connections, so that's 100. Cron jobs and an admin tool add five more, for 105. Postgres refuses. No single decision was wrong. The multiplication was the problem, and nobody was watching the total.

  • Autoscale: 4 → 10 servers
  • Pool per server: 10 connections
  • Total: 10 × 10 = 100
  • Cron jobs and admin: +5 = 105
  • Result: FATAL: too many clients already

Should you just raise max_connections?

It's the obvious move: open the config and set max_connections to 10,000. It feels like a fix. It's a trap. Every connection is a whole process, so a higher limit doesn't add chairs to an empty room. It invites more hungry processes onto the same machine.

Memory grows with every process. Thousands of processes fight over a handful of CPU cores. Speed often drops under load. And leaks? A bigger limit just hides them longer. A leak will fill 10,000 slots too, only later, when the server is already struggling.

Here's what surprises most people: more connections rarely means more speed. Your server has limited cores and disk speed, and that's the real ceiling on parallel work. Past a point, connections behave like people crowded into a small kitchen, bumping into each other. That's contention over CPU, memory and locks, and it slows every query. A small set of busy connections often finishes more work than a huge idle crowd. Aim for busy, not many.

  • Memory: grows with every process vs. stays small and steady with pooling
  • CPU: processes fight over cores vs. little contention
  • Speed: often slower under load vs. usually faster
  • Leaks: hidden longer vs. surfaced sooner

Find idle connections with pg_stat_activity

Down right now? Before changing anything, look. Postgres has a built-in view called pg_stat_activity that lists every connection and what it's doing. It's the guest list for your restaurant: who's eating, and who's been staring at an empty plate for an hour.

Group connections by state. Active means running a query right now. Idle means connected but doing nothing. If idle dominates, connections are being opened and abandoned. The view also shows the application name and client address, which turns a vague outage into a specific suspect, like one worker service hogging fifty slots.

You can end stuck sessions to free seats and get users back in fast. Treat that as a bandage. If the code is still leaking, those slots fill right back up.

# Who is connected, and what are they doing?
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
# Many idle rows? Something isn't closing.

Stop connection leaks: always close what you open

Now fix the leak at the source. The rule is simple: every connection you borrow must be given back, even when something goes wrong. Leaks rarely happen on the happy path. They happen when a query throws, the function exits early, and the cleanup line never runs.

The safe pattern: grab a connection, do the work inside try, release it inside finally. Finally always runs, whether the query succeeds, fails or throws halfway through. Most languages have a helper for this. JavaScript has finally, Python has with, C# has using. Lean on them so cleanup happens even on the days you forget.

Add a safety net too. Postgres can close sessions that sit idle too long, including ones stuck idle inside a transaction. Bugs will still happen, but they can't hold seats forever.

const client = await pool.connect();
try {
  await client.query(sql);
} finally {
  client.release();
}

How connection pooling with PgBouncer works

Here's the real fix. Instead of every client getting its own expensive process, let many clients share a few real connections. That's connection pooling, and PgBouncer is the classic tool for it. It sits between your app and Postgres. Your app thinks it's talking to the database, but it's talking to a small, efficient middleman built to juggle lots of clients.

On the client side, PgBouncer is cheap. It doesn't spawn a process per client, so holding many connections costs it very little, and that's the side serverless and spikes hit hardest. On the database side, Postgres sees a small, steady number of connections. Fewer processes, less memory, less contention.

Follow one query. Your app connects to PgBouncer, often just by changing the host and port in its settings. When a query arrives, PgBouncer lends it a free real connection. If all are busy, the query waits briefly instead of failing. In transaction mode, the connection goes back the moment the transaction ends. Waiting a few milliseconds beats a FATAL error every time.

  • Client connects to PgBouncer instead of Postgres
  • PgBouncer lends a free real connection for the query
  • In transaction mode, it's returned as soon as the transaction ends

Right-size your pools and keep the error from coming back

Your app probably has its own pool built into the database driver, and its size matters more than most people think. The instinct is to make it huge, just in case. But bigger pools mostly mean bigger crowds. Do the multiplication: pool size times app instances, plus workers and tools, must stay comfortably below max_connections.

Start modest and measure. If requests queue for a connection while the database looks relaxed, grow the pool a little. Otherwise, leave it alone. Plan for your busiest day, not your average one, because autoscaling multiplies your pools automatically.

Then avoid the habits that bring the error roaring back. Graph connection counts over time: a slow, steady climb is a leak in progress. And load test with autoscaling turned on before launch. Better to meet the wall in a Tuesday rehearsal than on sale day.

  • Don't raise max_connections to hide errors; pool first
  • Send serverless traffic through a pooler
  • Release connections in finally, even on error paths
  • Keep every app pool small and deliberate

The bottom line

  • The Postgres connection limit defaults to 100 slots, shared by everything that connects.
  • Each connection is a full OS process, and idle ones still use memory.
  • Raising the limit adds memory pressure and contention while hiding leaks.
  • Use pg_stat_activity to find idle connections, then fix the code that leaks them.
  • Pool with PgBouncer and keep app pools small, with headroom for your busiest day.

Quick-fire questions

What does 'FATAL: sorry, too many clients already' mean in Postgres?

It means every available connection slot is in use, so Postgres rejects the new connection immediately. It doesn't queue the request. The database itself is usually healthy; it's just full.

What is the default max_connections in Postgres?

The default is 100. By default, three of those slots are reserved for superusers, so regular application connections get a little less than that.

Is it safe to increase max_connections to fix the error?

It often postpones the crash rather than preventing it. Every connection is a separate process, so more connections mean more memory use and more contention for CPU and locks, which can make queries slower. Pooling is usually the better fix.

How do I find which app is using up Postgres connections?

Query the pg_stat_activity view and group connections by state. Look for large numbers of idle connections, then check the application name and client address to identify which service is holding them.

Why use PgBouncer if my app already has a connection pool?

Your app's pool only limits connections per instance, and autoscaling or serverless multiplies those pools. PgBouncer lets many clients share a small set of real Postgres connections, so the database sees a steady number no matter how many app instances are running.

Watch the full video on YouTube →

Souy Soeng

Souy Soeng

Hi there 👋, I’m Soeng Souy (StarCode Kh)
-------------------------------------------
🌱 I’m currently creating a sample Laravel and React Vue Livewire
👯 I’m looking to collaborate on open-source PHP & JavaScript projects
💬 Ask me about Laravel, MySQL, or Flutter
⚡ Fun fact: I love turning ☕️ into code!

Post a Comment

CAN FEEDBACK
Ad