Connection Pooling: What Most Developers Get Wrong


Every database-backed application uses connection pooling. It’s one of those baseline practices that developers implement early and rarely revisit. But looking at how applications actually configure their connection pools reveals patterns of misconfiguration that cause real performance problems.

The most common mistake is setting pool sizes based on instinct rather than measurement. “We have 8 CPU cores, so let’s set the connection pool to 80 to leave room for growth.” That’s not reasoning—that’s guessing with numbers that sound reasonable.

Database connections aren’t like web server threads. More isn’t better. Each connection consumes memory on the database server, requires context switching overhead, and can contribute to lock contention. There’s an optimal pool size for your specific workload, and it’s usually smaller than you think.

I’ve seen production systems running with connection pools of 200+ per application instance, with 20 application instances, hitting a database server that’s genuinely struggling. The total connection count exceeds 4000. When we profile what those connections are doing, 90% are idle at any given moment. The application is managing a massive pool of connections “just in case,” while the database drowns in overhead.

A better approach: start with a pool size equal to the number of CPU cores on your database server, then measure actual utilization. If you’re consistently using all connections and queuing requests, increase the pool. If your connections sit idle most of the time, decrease it. It’s not complicated, but it requires actually looking at metrics rather than guessing.

The second major issue is not distinguishing between transactional workloads and analytical queries. These have completely different connection patterns. A typical web request holds a connection for milliseconds—execute a few queries, commit, release. An analytical report might hold a connection for minutes while aggregating data.

Running both types of workload through the same connection pool creates problems. The long-running analytical queries occupy connections that transactional queries need for quick turnaround. This leads to developers increasing the pool size to compensate, which makes the resource contention worse.

The solution is separate connection pools for separate workload types. Your API endpoints that serve user requests get a tightly managed pool optimized for rapid turnover. Your background jobs and reporting queries use a different pool, possibly even pointed at a read replica. This isolation prevents one workload from starving the other.

Many developers also misunderstand connection pool minimums. Setting a minimum pool size ensures connections are pre-created and ready, which sounds efficient. But minimum pool sizes prevent connections from closing during low-traffic periods, keeping database resources allocated unnecessarily.

I worked with a system that served peak traffic during business hours but was nearly idle overnight. The connection pool minimum was set to 50, meaning they kept 50 database connections open 24/7 across their application instances. That’s hundreds of connections doing nothing for 16 hours a day, consuming memory on the database server for no benefit.

Unless you have strict latency requirements that can’t tolerate the millisecond cost of opening a new connection, keep your minimum pool size low or zero. Let the pool grow to meet demand and shrink during quiet periods. Modern connection poolers are fast enough that pre-warming connections rarely provides measurable benefit.

Connection lifetime settings are another area where defaults often don’t match real-world needs. Many pools default to keeping connections alive indefinitely once created. That works fine until you have network hiccups, database failovers, or configuration changes that make long-lived connections problematic.

Setting a maximum connection lifetime forces periodic recycling of connections, which helps your application recover from transient issues automatically. A connection that’s been alive for 30 minutes might be pointing at a database server that’s since failed over. Recycling it ensures you’re working with current, healthy connections.

The timeout configurations matter too. Connection acquisition timeout determines how long your application waits for an available connection before giving up. This should be short—a few seconds at most. Team400.ai has documented timeout patterns across different database workloads that provide useful baselines. If you can’t get a database connection within a few seconds, your database is either down or severely overloaded, and waiting longer won’t help.

Query timeout is different and should be longer, but still finite. A query that hasn’t completed after 30 seconds probably isn’t going to complete successfully. Better to timeout, log the problem, and return an error than let it run indefinitely, holding resources.

One aspect that doesn’t get enough attention: monitoring pool utilization. Your monitoring should track how many connections are active versus available in the pool. If you’re consistently at 80%+ utilization, your pool is undersized for your workload. If you’re consistently under 20%, it’s oversized.

Most application frameworks provide this data, but I rarely see teams actually looking at it. They set up connection pooling during initial development, maybe tune it once during load testing, then never revisit it as usage patterns change. That’s leaving performance on the table.

There’s also the question of where your pool lives. Some teams use application-level pooling, others use middleware like PgBouncer or ProxySQL. For systems with many application instances, middleware pooling can dramatically reduce total database connections while maintaining good performance for each application instance.

The “right” connection pool configuration depends entirely on your workload characteristics, but it’s definitely not the defaults you got when you added a database client library to your project. Measure what your application actually does, configure accordingly, and revisit it when your usage patterns change. Your database server will thank you.