Skip to the solution
MediumSystem Design13 min read

What is database connection pooling?

885 views

The problem

Question presented to candidate: "Explain database connection pooling. Why is opening a connection per request a problem, how does a pool work, and how would you choose the pool size? What changes when you run fifty instances of the service, or run it on serverless functions?"

What a strong answer should cover:

  • A connection is expensive to open (TCP handshake, TLS, authentication, and in PostgreSQL a whole new backend process per connection) and costs the server memory while it is open
  • A connection pool keeps a set of open connections; requests borrow one, run their queries and return it; when all are busy, requests wait in a queue with an acquire timeout
  • Sizing: more connections than the database can execute in parallel adds contention, not throughput; use Little's Law for the minimum and a small multiple of DB cores (HikariCP: cores * 2 + effective spindles) as a starting point, then measure
  • Fleet math: instances x pool size must stay below the database max_connections (PostgreSQL default 100); with many instances or serverless, add an external pooler such as PgBouncer
  • PgBouncer session vs transaction vs statement mode, and what transaction mode breaks (session state, LISTEN, SQL-level PREPARE)
  • Hygiene: return connections promptly (never hold one across a slow HTTP call), max lifetime, idle timeout, leak detection, health checks

Clarifying questions expected:

  • How many app instances, and how many concurrent requests per instance at peak?
  • What database, and how many cores and max_connections does it have?

Code / implementation expected: Optional โ€” a description of the pool and a sizing argument; the example simulates pool sizes on a fake clock.

  • connection-pool

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

13 min read ยท 10 sections

Read the code

Connection pool sizing on a fake clock: no pool vs too small vs right-sized vs too big
// Discrete simulation with a 1 ms fake clock: an app sends queries to a database
// with 8 CPU cores. Each query needs 4 ms of CPU. Running more queries at once than
// there are cores does not add capacity; it adds contention (context switches, locks).
const CORES = 8, WORK = 4, CONNECT_MS = 20, CONNECT_CPU = 1, CONTENTION = 0.005;

function arrivals(light) {
  const out = [];
  if (light) { for (let t = 0; t < 1000; t += 10) out.push(t); return out; } // 100 req/s
  for (let t = 0; t < 100; t++) for (let k = 0; k < 4; k++) out.push(t); // burst: 4000 req/s for 100 ms
  for (let t = 100; t < 1000; t++) out.push(t);                         // then 1000 req/s
  return out;
}

function simulate(poolSize, light) {          // poolSize 0 means: open a new connection per request
  const reqs = arrivals(light);
  const queue = [], connecting = [], active = [], latencies = [];
  let next = 0, free = poolSize, maxConns = 0, t = 0;
  while (latencies.length < reqs.length) {
    while (next < reqs.length && reqs[next] === t) {
      if (poolSize === 0) connecting.push({ arrived: t, ready: t + CONNECT_MS });
      else queue.push({ arrived: t });
      next++;
    }
    while (connecting.length > 0 && connecting[0].ready === t) {
      const r = connecting.shift();
      active.push({ arrived: r.arrived, work: WORK + CONNECT_CPU }); // the server also pays to set up a session
    }
    while (poolSize > 0 && free > 0 && queue.length > 0) {
      active.push({ arrived: queue.shift().arrived, work: WORK });
      free--;
    }
    maxConns = Math.max(maxConns, poolSize === 0 ? connecting.length + active.length : poolSize - free);
    const k = active.length;
    if (k > 0) {
      const efficiency = 1 / (1 + CONTENTION * Math.max(0, k - CORES));
      const share = Math.min(1, (CORES * efficiency) / k);
      for (const q of active) q.work -= share;
      for (let i = active.length - 1; i >= 0; i--) {
        if (active[i].work <= 1e-9) {
          latencies.push(t + 1 - active[i].arrived);
          active.splice(i, 1);
          if (poolSize > 0) free++;
        }
      }
    }
    t++;
  }
  latencies.sort((a, b) => a - b);
  const pct = (q) => latencies[Math.ceil(q * latencies.length) - 1];
  return { p50: pct(0.5), p99: pct(0.99), max: latencies[latencies.length - 1], done: t, maxConns };
}

console.log(`Load: ${arrivals().length} queries; burst of 400 in the first 100 ms, then 1 per ms until t=1000 ms`);
console.log(`Database: ${CORES} cores, ${WORK} ms CPU per query; new connection = ${CONNECT_MS} ms handshake + ${CONNECT_CPU} ms server CPU`);
console.log("");
console.log("pool size        p50 ms  p99 ms  max ms  all done at  peak DB connections");
for (const size of [0, 2, 8, 16, 64, 400]) {
  const r = simulate(size, false);
  const name = size === 0 ? "none (connect)" : String(size);
  console.log(`${name.padEnd(15)} ${String(r.p50).padStart(7)} ${String(r.p99).padStart(7)} ${String(r.max).padStart(7)} ${String(r.done).padStart(9)} ms ${String(r.maxConns).padStart(10)}`);
}

console.log("");
console.log(`Light load: ${arrivals(true).length} queries, 1 every 10 ms (no queueing, no contention)`);
for (const size of [0, 8]) {
  const r = simulate(size, true);
  console.log(`  pool ${size === 0 ? "none (connect)" : size}: p50 ${r.p50} ms, p99 ${r.p99} ms`);
}

console.log("");
console.log("Fleet arithmetic: why per-instance pools still overload the database");
const instances = 40, perInstance = 10, maxConnections = 100;
console.log(`  ${instances} app instances x pool ${perInstance} = ${instances * perInstance} connections > max_connections ${maxConnections}`);
const poolerServerSide = 20;
console.log(`  with a transaction-mode pooler: ${instances * perInstance} client connections share ${poolerServerSide} server connections`);
console.log(`  HikariCP starting point for ${CORES} cores, data cached: ${CORES} * 2 + 0 = ${CORES * 2} connections`);

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 15 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextMedium

What is the publish-subscribe pattern?

Continue