Databases
Database Connection Pooling: Protect Capacity at Scale
Database connection pooling protects capacity as traffic grows. Size pools across every instance, bound waits, and test saturation before requests fail.
TL;DR — Database connection pooling reuses a controlled number of database connections instead of opening one for every request. It reduces connection setup work and, more importantly, puts a ceiling on concurrent database demand. The safe pool is sized from database capacity and deployment topology, never from “more connections must be faster.”
Every application request that reaches a database needs a connection or a place to wait. A connection is not free: it uses memory, authentication work, and backend capacity. When several web instances each create their own generous pool, the sum can exceed what the database can serve long before CPU graphs look alarming.
A pool creates a queue. When all connections are busy, a new request waits rather than immediately creating another backend process. That makes the trade-off visible: a short wait may protect the database; an unbounded queue hides overload until timeouts spread through the product.
Begin with a capacity budget
First reserve connections for database administration, migrations, background work, and known integrations. Then divide the remaining budget across application instances and pools. The arithmetic is deliberately simple: if a database can safely allocate a chosen application budget of 80 connections and you run four web instances, a starting maximum of 20 per instance is the upper bound before other pools are counted. It is only a hypothetical planning calculation; measure real transaction duration and concurrency before setting production values.
PostgreSQL’s connection settings explain that max_connections controls concurrent connections, while PgBouncer documents pooling modes that can reduce the number of server connections. Those are tools, not a sizing answer. The answer comes from your actual query costs, workload mix, and number of processes that can connect at once.
Keep the checkout and return boring
Application code should acquire a connection late, use it for a short transaction, and return it promptly. Do not hold a database connection while calling a payment provider, rendering a document, or waiting for a customer upload. A pool cannot compensate for an endpoint that holds scarce connections during unrelated work.
Transaction pooling adds a specific constraint: session settings, temporary tables, prepared statements, and advisory locks may not survive from one transaction to the next. If your application relies on session state, choose a compatible pooling mode or change the interaction. This is the kind of hidden coupling that a staging environment should expose before a scale event.
A hypothetical incident to design against
Imagine eight application instances, each configured with 30 connections, pointed at a database that has room for 120 application connections. A deployment doubles the instances briefly. The theoretical demand becomes 480 connections before queued jobs and reporting are counted. Some requests fail to connect, retry, and amplify the surge. The fix is not simply a higher database limit; it is a deployment-aware budget, bounded retries, and visibility into pool wait time.
Load testing can reveal the difference between a slow query and a saturated pool, but only if it records queue wait, connection use, and database latency separately. An index change can reduce how long a connection is held; see database indexes.
Failure modes founders should insist on seeing
- Pool limits are configured per instance but never totaled across instances.
- Idle connections consume the whole database budget during an autoscaling event.
- A timeout discards the request while the database work keeps running.
- A retry opens more work against the same overloaded pool.
- Monitoring shows database CPU but not pool wait, checkout timeout, or active connections.
Acceptance checklist
- There is one documented connection budget across web, worker, migration, and reporting clients.
- Each process has a bounded pool, wait timeout, and retry policy.
- Long external calls happen outside database transactions.
- Metrics expose active, idle, waiting, and timed-out pool requests.
- A staged load test covers normal traffic and a deployment or scale transition.
Also rehearse the response to saturation. Decide whether the product should reject low-priority background work, show a retryable message, or queue the action. That decision belongs to product and operations together; it cannot be recovered by changing a pool number during an incident.
Pooling is a reliability control. Its success is not a high connection count; it is a database that remains responsive when demand rises.
Related paths