Connection Pooling
Opening a database connection is expensive — a TCP handshake, authentication, and session setup — and databases cap how many connections they'll accept. A connection pool solves both: it maintains a small set of open connections and lends them out to requests, recycling each when the work is done. Every page in this section has invoked the "pool once" rule; this page explains why it matters, how to size a pool, and the failure modes when you get it wrong.
The problem a pool solves
Without a pool — a new connection per request:
request → [TCP handshake + auth + setup ~10-50ms] → query → close
↑ paid on EVERY request; DB connection limit hit under load
With a pool — N connections, created once, reused:
request → borrow a ready connection → query → return it to the pool
↑ handshake paid once; requests share a small, stable setCreating a pool — once, at startup
Postgres (pg)
import { Pool } from 'pg'
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // max connections in the pool
min: 0, // keep this many warm (driver-dependent)
idleTimeoutMillis: 30_000, // close idle connections after 30s
connectionTimeoutMillis: 5_000, // fail fast if none free within 5s
})Borrowing for transactions vs simple queries
// Simple query — pool.query borrows + returns a connection automatically:
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id])
// Transaction — check out ONE connection and release it in finally:
const client = await pool.connect()
try {
await client.query('BEGIN')
/* ...multiple statements on the SAME connection... */
await client.query('COMMIT')
} catch (e) {
await client.query('ROLLBACK'); throw e
} finally {
client.release() // ← return it, ALWAYS
}Sizing the pool
Factor | Guidance |
|---|---|
Database | Pool size × instances must stay UNDER it |
Number of app instances | Each instance has its own pool — multiply |
Workload | I/O-bound queries need fewer connections than you'd think |
Starting point | ~10 per instance; tune with metrics, don't guess high |
Serverless changes the math
Traditional server: a few long-lived instances, each with one pool. Serverless (Lambda/Edge): MANY short-lived instances, each opening connections → can blow past the DB limit during a traffic spike. Fix: an external pooler that fronts the database: functions → [ PgBouncer / Prisma Accelerate / RDS Proxy ] → Postgres
Health: monitor and handle failures
Listen for pool error events (a backend connection dropped) and log them.
Set
connectionTimeoutMillisso requests fail fast instead of hanging when the pool is exhausted.Validate/reconnect after the DB restarts; good drivers do this, but verify.
Track pool metrics (total/idle/waiting) — a growing waiting count signals an undersized pool or a leak.
Drain the pool on shutdown (
pool.end()) so connections close cleanly.