Skip to main content
Databases

Database connection pooling: why your app crashes at 100 users

A database pool is a concurrency budget, not a performance toggle. Size it across all replicas, queue work deliberately, and stop each process from opening a tiny denial-of-service attack.

Thien Nguyen
By Thien Nguyen
Updated July 21, 2026 · 2 min read

One application instance with a pool of 20 connections looks harmless. Twenty instances behind an autoscaler look like 400 connections pointed at a database configured for 200. The outage is not mysterious: every request is waiting for a scarce resource while the database spends its remaining time context-switching.

The fix is unglamorous: size a small pool per process, multiply it by the maximum number of replicas and workers, leave room for migrations and admin access, and make pool wait time observable. A pool limits concurrent database work; it does not make the database able to do infinite work.

CapacityExample
Database max_connections200
Reserved for admin/migrations20
Maximum app replicas15
Safe starting pool per replica10–12

That table is a budget, not a universal recommendation. A serverless deployment needs special care because instances can multiply faster than connection limits. Put a proxy or database-native pooling layer between short-lived workers and the database when direct connections do not fit the execution model.

What to measure before raising the limit

  1. Pool checked-out count and wait time.
  2. Query latency by operation, not just HTTP latency.
  3. Idle-in-transaction sessions and their age.
  4. Database CPU, lock waits, and active connections.

A larger pool often converts application queueing into database queueing. If queries are slow or transactions stay open, more connections magnify the contention.

The request path that behaves

const client = await pool.connect();
try {
  return await client.query("select * from orders where id = $1", [id]);
} finally {
  client.release();
}

Always release in finally; a rare thrown error is enough to exhaust a pool under load. Hold a transaction only around the statements that need atomicity, never around an HTTP call or slow third-party API.

Connection pooling is capacity planning in miniature. Treat it as shared infrastructure, and the “100 users” cliff becomes a visible limit you can manage.

Size for the busiest deployment shape

The pool budget must include web processes, background workers, preview environments, and migration tooling. If an autoscaler can briefly double replicas, use that maximum—not the comfortable steady-state number. Set an acquisition timeout so a saturated pool fails a request quickly and visibly instead of letting hundreds of promises wait until the process runs out of memory.

Serverless needs a different boundary

Opening a direct database connection per invocation works in a local test and collapses under concurrent cold starts. Use the database provider's supported proxy or a pooling service, and keep transactions short. A pool reduces connection churn; it cannot make slow queries, lock contention, or idle transactions disappear.

Cover photo by panumas nikhomkhai on Pexels.

References

Primary documentation and specifications checked when this article was last updated.

DatabasesPerformanceReliability

Related articles

All articles