Academy · Databases & Replication

Connection pooling

Opening a fresh DB connection per request is expensive, and databases cap how many they'll accept.

Open the interactive version → diagrams, practice & more

The problem

Opening a fresh DB connection per request is expensive, and databases cap how many they'll accept.

The idea

Reuse a pool of pre-opened connections instead of creating one each time.

How it works

A pooler (PgBouncer or an in-app pool) keeps N warm connections and hands them out, removing per-request setup and capping concurrency at the DB. Sizing is counter-intuitive — more connections ≠ more throughput. Each Postgres connection is a backend process with real memory; beyond ~(cores × 2–4) they mostly context-switch and contend, so a small pool often beats a large one. Transaction-mode pooling multiplexes many clients onto few server connections — essential for serverless, where thousands of function instances would each open connections.

The tradeoff

Too small a pool throttles throughput and queues requests; too large overwhelms the DB with context-switching and memory pressure, and can deadlock when every connection waits on another. The sweet spot is small and tuned to DB cores, not client demand — and a transaction-mode pooler decouples client count from server connections entirely.

In the wild

Serverless functions infamously exhaust DB connections without a pooler in front.

Interview deep dive

Flow

  1. Put a pooler (PgBouncer/in-app) in front of the database.
  2. Size the pool near DB cores (×2–4), not client count.
  3. Use transaction-mode pooling for serverless / high client fan-out.
  4. Monitor wait time and saturation; tune rather than enlarge.

Watch for

Interviewer trap

Size the pool to DB cores and name transaction-mode pooling for serverless connection storms.

Related Academy

Part of Academy on SystemLore — system design interview prep with 148 deep topics, interactive diagrams, and a practice game. Practice this one →