Connection Pooling
Connection pooling reuses a bounded set of database or upstream connections, avoiding per-request handshakes while keeping concurrency within the dependency's limit.
Definition
A connection pool maintains a set of open connections that requests borrow and return. Opening a database connection involves network handshakes, authentication, and session setup, which is expensive relative to a simple query. Reusing connections reduces latency and protects the dependency from an unbounded number of concurrent sessions. The pool size is the real concurrency limit.
Context and scope
Pools are used for relational databases, Redis, HTTP clients, and gRPC channels. They are especially important in serverless or autoscaled environments, where the number of application instances can exceed the database's connection limit.
Common misconceptions
- A larger pool always improves throughput — beyond the dependency's capacity it increases contention and latency
- A pool is per database — the limit applies to the total sessions the database can serve, including all instances
- Connection reuse means transactions are shared — a connection returned to the pool must be clean and not carry an open transaction
Anti-patterns
- Sizing each of 20 instances with a 50-connection pool against a database that allows 100 sessions
- Returning a connection to the pool without rolling back an aborted transaction
- Holding a connection while waiting on an external API or user input
Related concepts
Related examples
Authoritative source
The expensive part is the handshake
A database connection requires a TCP handshake, TLS negotiation, authentication, and session initialization. Doing that for every request adds latency and CPU on both sides. A pool keeps a small number of connections open and lends them to requests. The request borrows, queries, and returns the connection.
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 2_000,
});
const result = await pool.query('SELECT id, name FROM users WHERE id = $1', [id]);
max is not a tuning suggestion; it is the maximum concurrent sessions this process will open. If the database allows 100 connections and you run 20 instances, a pool of 10 per instance already exceeds the limit. Set a per-instance budget and sum it across the deployment.
Size for the dependency, not the request rate
A pool larger than the database can serve just moves the queue server-side and increases context switching. A pool smaller than the workload rejects or queues requests earlier, which is often healthier than overwhelming the database. Measure query latency, wait time for a connection, and timeout rate. Increase the pool only when the database has headroom and the wait time is the bottleneck.
Return clean connections
A connection returned with an open transaction leaks locks and state to the next borrower. Always commit or roll back, and use the driver’s release mechanism in a finally block. Set a statement timeout so a runaway query does not occupy a connection indefinitely. For long-running work, use a dedicated connection or a job system rather than holding a pooled connection.
When to use it
Use a pool for any shared remote connection with a meaningful setup cost. The advantage is lower latency and bounded concurrency; the drawback is that the pool size and timeout policy become part of capacity planning. Compared to opening a connection per request, pooling is more efficient but requires discipline around cleanup and total connection budgets.