Connecting to a database from Node — connection pooling and why it matters

Opening a fresh database connection for every single request is genuinely slow and can exhaust the database's own connection limit under real traffic — a connection pool exists specifically to solve both problems by reusing a small, fixed set of already-open connections.

Intermediate

4 min read

The naive approach: a new connection per request, and why it's genuinely expensive

app.get("/orders", async (req, res) => {
  const connection = await createConnection(dbConfig); // opens a BRAND NEW TCP connection, every request
  const orders = await connection.query("SELECT * FROM orders");
  await connection.close();
  res.json(orders);
});

Opening a database connection is real, measurable work — a TCP handshake, often TLS negotiation, and the database server's own connection-setup overhead — happening freshly on every single request. Under real traffic, this is both slow (each request pays that setup cost) and dangerous: most databases have a hard maximum on how many simultaneous connections they'll accept at all, and a traffic spike opening thousands of brand-new connections can exhaust that limit, causing the database to start rejecting connections entirely — a real, common cause of a production outage that looks like "the database is down" when it's actually "too many connections were opened too fast."

A connection pool: a small, fixed set of connections, reused across requests

const pool = createPool({ ...dbConfig, max: 10 }); // opens (at most) 10 connections, ONCE, at startup
 
app.get("/orders", async (req, res) => {
  const orders = await pool.query("SELECT * FROM orders"); // BORROWS an existing connection from the pool
  res.json(orders);                                          // automatically RETURNED to the pool when done
});

A pool opens a small, bounded number of real connections once, up front, and hands one out to each request that needs it — returning it to the pool (not closing it) once the query finishes, ready for the next request to reuse. This eliminates the per-request connection setup cost entirely (the connection already exists) and caps the total number of simultaneous connections at a known, deliberate limit (max: 10 here), regardless of how many concurrent requests are actually being handled.

Choosing a pool size: a real trade-off, not "bigger is always better"

Too small a pool: requests QUEUE, waiting for a connection to free up —
  under real load, this becomes a genuine bottleneck, even though the
  database itself could easily handle more simultaneous queries

Too large a pool: risks exceeding the database's OWN connection limit,
  especially once multiplied across every separate server instance
  running the same app (10 instances × a pool of 50 = 500 real connections)

A pool that's too small becomes an artificial bottleneck — requests wait in a queue for a connection to become free even though the database itself has spare capacity. A pool that's too large risks exceeding the database's actual connection ceiling, especially once the same pool size is multiplied across every running instance of a horizontally-scaled app (a real, common miscalculation: sizing a pool for one instance's expected load, then deploying ten instances, each opening that same pool size). The right size genuinely depends on the database's own connection limit divided by the number of concurrent app instances, not a single universal number.

What happens to an in-flight query if the pool is exhausted

const pool = createPool({ ...dbConfig, max: 10, connectionTimeoutMillis: 5000 });
// A request arriving when all 10 connections are busy WAITS, up to 5 seconds,
// for one to free up — then throws a timeout error if none becomes available in time

When every connection in the pool is currently in use, a new request needing one doesn't fail immediately — it waits in an internal queue, up to a configurable timeout, hoping a connection frees up in time. This is a real, deliberate design choice worth configuring explicitly: an unbounded wait can make a struggling database's problems cascade into every waiting request hanging indefinitely, while a connectionTimeoutMillis that's too short can produce timeout errors under load spikes that would have resolved themselves in another second or two — a real trade-off, not a value with an obviously correct default for every application.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why is opening a new database connection for every single request genuinely expensive?

2. What does a connection pool actually do differently from opening a fresh connection per request?

3. Why is 'bigger pool size is always better' a wrong assumption?