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
2
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.
3
Solution
Target Audience: Backend beginners โ you have written code that queries a database; no knowledge of database internals assumed.
Difficulty: Medium
Verified with: Node 22.22 and Python 3.12 (both print identical output).
How to read this doc: Every idea is explained in plain language first, then tagged with ๐ Interview term: so you also learn the exact words an interviewer expects. Behaviour shown comes from running the example; the Verified section quotes the full output.
1. Why This Even Matters โ A Story First
A busy restaurant could hire a new waiter for every customer who walks in: interview them, train them, hand over a uniform, and dismiss them when the customer leaves. Each customer waits twenty minutes before ordering, and on a Friday night the kitchen is buried under three hundred waiters shouting orders at eight cooks.
A sane restaurant keeps ten
The solution is hidden so you can try first
13 min read ยท 10 sections
4
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).constCORES = 8, WORK = 4, CONNECT_MS = 20, CONNECT_CPU = 1, CONTENTION = 0.005;
functionarrivals(light) {
const out = [];
if (light) { for (let t = 0; t < 1000; t += 10) out.push(t); return out; } // 100 req/sfor (let t = 0; t < 100; t++) for (let k = 0; k < 4; k++) out.push(t); // burst: 4000 req/s for 100 msfor (let t = 100; t < 1000; t++) out.push(t); // then 1000 req/sreturn out;
}
functionsimulate(poolSize, light) { // poolSize 0 means: open a new connection per requestconst 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);
constpct = (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`);
5
Discussion
Other approaches, follow-ups and how it went in real interviews.