Photo by Albert Stoynov on Unsplash
Why Database Connection Limits Matter More Than You Think
Most applications treat database connections as an unlimited resource until the day production grinds to a halt with “too many connections” errors. Understanding why databases impose hard connection limits—and why those limits are surprisingly low—is essential for building systems that scale reliably.
The Unexpected Bottleneck
Postgres defaults to 100 concurrent connections. MySQL to 151. These numbers feel shockingly small compared to the thousands of requests per second a modern web application might handle. The reason isn’t arbitrary: every database connection consumes significant resources on the server.
Each connection requires dedicated memory for buffers, query parsing, and connection state. A Postgres connection typically consumes 5-10 MB of RAM just for overhead. More critically, connections tie up OS-level resources like file descriptors and thread or process slots. At scale, hundreds of idle connections sitting in application pools across multiple servers can exhaust the database’s capacity long before CPU or disk become bottlenecks.
Why Applications Overwhelm Connection Pools
The typical three-tier architecture creates a multiplier effect. If you run 10 application servers, each with a connection pool sized to 20, you’ve already consumed 200 connections—double Postgres’s default limit. Add in background job workers, scheduled tasks, and a few forgotten maintenance scripts, and the math gets worse fast.
Developers often size connection pools based on local testing or single-instance assumptions. A pool of 20 feels reasonable for one app server handling concurrent requests. But that sizing decision, multiplied across a horizontally scaled deployment, becomes the bottleneck. The database sees the aggregate load, not the per-instance logic.
The Wrong Solutions That Make It Worse
The intuitive fix—raising the database’s max_connections setting—often creates more problems than it solves. Increasing the limit doesn’t add capacity; it just allows more clients to compete for the same underlying resources. With 500 or 1,000 connections active, context switching overhead degrades performance for everyone. Query latency increases, CPU thrashes between connection handling, and the database becomes less efficient at the actual work of executing queries.
Some teams discover this the hard way during incidents: raising connection limits temporarily relieves the immediate error, but overall throughput drops and p99 latency spikes. The system trades one failure mode for another.
Connection Poolers as Middlemen
Connection pooling at the database level—through tools like PgBouncer for Postgres or ProxySQL for MySQL—provides a more sustainable approach. These poolers sit between applications and the database, multiplexing hundreds or thousands of application connections onto a much smaller set of actual database connections.
When an application executes a query, the pooler assigns it one of the real connections, executes the query, and returns the connection to the pool. The application maintains its own connection to the pooler, but the database only sees a controlled number of active connections. This architecture decouples application scaling from database connection consumption.
The tradeoff is transaction and session state management. Poolers in transaction mode allow applications to use transactions normally but still require one database connection per active transaction. Session-level features like prepared statements or temporary tables may not work transparently across pooled connections. Choosing the right pooling mode depends on your application’s use of these features.
Right-Sizing From the Start
Effective connection management starts with understanding actual concurrency needs. Most requests spend minimal time holding a database connection—executing a query takes milliseconds, while the request spends most of its time in application logic or waiting on external APIs. A single database connection can serve dozens of requests per second if applications acquire and release connections efficiently.
Connection pool sizing should account for true concurrency: how many simultaneous database operations your application genuinely requires. For many workloads, small pools—often 5 to 10 connections per application instance—deliver better performance than larger ones by reducing contention and keeping the database focused on active work.
The architecture matters more than the numbers. Centralizing connection pooling, monitoring connection usage across the fleet, and treating connections as the scarce resource they are prevents the category of outages that end with frantic config changes and emergency database restarts.