Connection Pooling
Cloud infrastructurecloud infrastructure / databases
The scenario
Your API runs great in dev. You deploy to production on serverless (Lambda / Cloud Run), traffic hits ~300 req/s, and Postgres starts throwing:
FATAL: sorry, too many clients already
You restart the DB. It works for 2 minutes, then dies again. CPU is low, queries are fast. So what's killing it?
The diagnosis
Each request opened its own database connection. Postgres' default max_connections is ~100. Worse: opening a TCP+TLS+auth handshake to Postgres costs ~1–3ms and real server memory (each backend = several MB of RAM). At 300 concurrent requests you needed 300 connections — and you only had 100.
The fix isn't "raise max_connections to 5000." That just moves the crash from "too many clients" to "out of memory," because every idle connection still holds RAM.
The fix: a pool
A connection pool keeps a small, fixed set of warm connections and lends them out:
// Node + pg
const pool = new Pool({ max: 10 }) // 10 connections, reused
const res = await pool.query('SELECT 1') // borrow → run → return
Request #11 doesn't open a new connection — it waits a few ms for one of the 10 to free up. 10 reused connections can serve thousands of req/s because each query only holds the connection for milliseconds.
The two layers people confuse
- App-side pool (pg.Pool, HikariCP): lives inside ONE process. Great for a long-running server.
- External pooler (PgBouncer, Supabase pooler, RDS Proxy): a proxy in front of the DB. Essential for serverless, where you have N short-lived instances that can't share an app-side pool — so each would otherwise hammer the DB directly.
The gotcha that bites intermediate devs
PgBouncer in "transaction mode" multiplexes connections per-transaction. That silently breaks:
- prepared statements / session-level SET
- LISTEN/NOTIFY
- advisory locks held across queries
…because your next query may land on a different backend. Rule of thumb: pool size ≈ a small multiple of CPU cores, NOT your request rate. More connections ≠ more throughput past that point — they just queue inside Postgres.
Your exercise
You run 4 app instances on serverless, each with an app-side pool of max:20, all pointing straight at a Postgres with max_connections=100.
- What's the worst-case total connection demand?
- Why does this still crash, and what single component would you add to fix it without touching the DB limit?