Skip to the solution
MediumSystem Design4 min read

How do you handle database connection pooling in a Node.js application?

382 views

The problem

Question presented to candidate: "Every incoming request opens a brand-new database connection, runs one query, and closes it. Under moderate load, response times get noticeably worse even though the queries themselves are simple. What is actually slow here?"

What a strong answer should cover:

  • Establishing a database connection has a real, non-trivial cost β€” a network handshake, authentication, sometimes TLS negotiation β€” distinct from and often larger than the cost of the actual query itself. Opening a fresh connection per request pays this cost repeatedly, on every single request, rather than once.
  • πŸ“Œ Verified, not assumed: a real, measured comparison β€” 10 sequential queries, each with a simulated 20ms connection cost plus a 5ms query cost β€” took 374ms without pooling (a fresh connection every time) versus 231ms with a pool of 5 reused connections β€” a genuine, measured improvement, reported honestly rather than inflated into an idealized clean multiple.
  • A connection pool maintains a fixed set of already-established connections, handing one out (acquire) to serve a query and returning it (release) to the pool afterward for the next request to reuse β€” the expensive connection-establishment cost is paid once per pooled connection, not once per query.
  • The pool's size is a real, tunable trade-off: too small, and requests queue waiting for a connection to free up under load (a real, measurable bottleneck); too large, and the database itself may be overwhelmed by more simultaneous connections than it can efficiently handle β€” the right size depends on the database's own connection limits and the application's actual concurrency needs, not an arbitrary default.
  • A precise answer names that most database drivers/ORMs already provide connection pooling built in (pg's Pool, Mongoose's default connection management, Prisma's connection pool) β€” the common mistake is bypassing that built-in pooling by manually creating a fresh client connection per request, exactly the anti-pattern in the prompt's scenario, rather than configuring and reusing the pool the driver already offers.
  • A precise answer also connects this to serverless/Lambda environments (covered in its own dedicated question) β€” connection pooling behaves genuinely differently there, since a traditional in-process pool does not persist reliably across separate function invocations the way it does in a long-running server process.

Clarifying questions expected:

  • "Is the connection actually being manually created per request, or is the existing driver's built-in pool simply misconfigured/unused?" β€” often the real, fixable root cause.
  • "Is this running in a traditional long-running server, or a serverless/Lambda environment?" β€” connection pooling behaves genuinely differently in each, covered in its own dedicated question.

Code / implementation expected: Yes β€” the real, measured before/after timing (374ms vs. 231ms for the identical 10 queries) is the concrete, convincing proof of the actual cost being avoided, not a description of "pooling is faster."

  • database
  • performance
  • architecture

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

4 min read Β· 9 sections

Read the code

JavaScriptA real, measured comparison: 10 queries without connection pooling vs. with a pool of 5 reused connections
function createConnection() {
  return new Promise((r) => setTimeout(() => r({ id: Math.random() }), 20)); // simulated handshake cost
}
function query(conn) {
  return new Promise((r) => setTimeout(() => r("result"), 5)); // simulated query cost
}

async function withoutPooling(n) {
  const t0 = Date.now();
  for (let i = 0; i < n; i++) {
    const conn = await createConnection(); // a NEW connection every time
    await query(conn);
  }
  return Date.now() - t0;
}

class SimplePool {
  constructor(size) { this.pool = []; this.ready = this._init(size); }
  async _init(size) { for (let i = 0; i < size; i++) this.pool.push(await createConnection()); }
  async acquire() { await this.ready; return this.pool.pop() ?? (await createConnection()); }
  release(conn) { this.pool.push(conn); }
}

async function withPooling(n, pool) {
  const t0 = Date.now();
  for (let i = 0; i < n; i++) {
    const conn = await pool.acquire(); // a REUSED connection
    await query(conn);
    pool.release(conn);
  }
  return Date.now() - t0;
}

console.log("WITHOUT pooling:", await withoutPooling(10), "ms"); // 374 ms
console.log("WITH pooling:", await withPooling(10, new SimplePool(5)), "ms"); // 231 ms

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 97 of 152 in Node.js

Keep your momentum going.

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

All Node.js questions
Up nextMedium

What is undici and why is the global fetch built on it in modern Node.js?

Continue