Databases & Backend

Database Pooling: 312% Throughput Gap Revealed

We dug into database connection pooling, and the results are stark. One strategy delivered over three times the throughput of another, proving that your default settings might be costing you dearly.

Diagram illustrating different database connection pooling strategies and their performance metrics.

Key Takeaways

  • Benchmarking seven database connection pooling strategies revealed a 312% throughput difference between the worst and best performing.
  • The default fixed-size connection pool performed poorly under burst loads, creating artificial bottlenecks.
  • Elastic and partitioned pools improved performance but introduced resource volatility or overprovisioning concerns.
  • Priority queue pools significantly improved latency for critical business operations, even if overall throughput was moderate.
  • The 'best' strategy depends heavily on the specific workload and business priorities, with hybrid approaches often yielding strong results.

The checkout endpoint was timing out. Not the database, not the application servers, but the checkout endpoint itself was gargantuanly slow, P99 latency soaring to an almost unimaginable 8.7 seconds during a recent Black Friday surge. And the irony? The database was barely breaking a sweat, hovering around 47% utilization. CPU on the app servers? A breezy 34%. The bottleneck wasn’t raw capacity; it was the humble, often overlooked, connection pool.

Our team had defaulted to HikariCP, a fine library, but had never actually subjected its configuration — or any other pooling strategy — to a rigorous benchmark. It was time. Three weeks later, armed with production-scale staging, a sophisticated load tester, and a healthy dose of skepticism, we put seven distinct connection pooling approaches through their paces.

The findings weren’t just interesting; they were, frankly, staggering. The delta between the worst and best performing strategy? A mind-boggling 312% difference in throughput. That’s not a typo. It means the right choice can turn a traffic bottleneck into a data highway, while the wrong one slams the brakes on your entire operation.

Here’s what we tested:

  • Naive Pool (Fixed Size): Your garden-variety, first-come, first-served fixed pool.
  • Dynamic Pool (Elastic): Grows and shrinks with demand.
  • Partitioned Pool: Separate pools for each database shard.
  • Priority Queue Pool: Critical tasks get preferential treatment.
  • Connection Borrowing: Temporary, on-demand connection grabbing.
  • Pre-warmed Pool: Connections kept at the ready.
  • Hybrid Adaptive: A blend of elastic sizing and priority queuing.

Our testbed? A PostgreSQL 14 instance on AWS RDS (r6g.4xlarge, 16 vCPU, 128GB RAM), simulating 50,000 concurrent users with a 3:1 read/write ratio. The traffic pattern was brutal: a 20% baseline, punctuated by sudden 500% spikes, and a mix of queries ranging from 10ms to a hefty 800ms.

The Default Trap: Fixed Size & Gridlock

The standard HikariCP configuration, with a fixed pool size and simple timeouts, looked like this:

HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(50);
config.setMinimumIdle(50);
config.setConnectionTimeout(5000);

Results:

  • Throughput: 2,847 req/sec
  • P99 latency: 8,743ms
  • Connection wait time: P99 = 6,200ms
  • Pool exhaustion events: 4,723
  • Failed requests: 18.4%

This is where our own pain point lived. Under load, requests piled up. Even with an underutilized database, the rigid pool size acted as a cork, forcing an artificial bottleneck. It’s the operational equivalent of having a ten-lane highway suddenly narrow to two.

Elasticity: More Horsepower, More Chaos?

Next up, the Dynamic (Elastic) Pool, aiming to adapt:

config.setMaximumPoolSize(200);
config.setMinimumIdle(20);
config.setIdleTimeout(60000);
config.setMaxLifetime(1800000);

Results:

  • Throughput: 4,183 req/sec (47% better)
  • P99 latency: 2,943ms (66% better)
  • Connection wait time: P99 = 840ms
  • Pool exhaustion events: 847
  • Failed requests: 6.2%

This was a clear win on raw throughput and a substantial drop in latency. But here’s the kicker: the resource usage was erratic. The pool would balloon to 180 connections during spikes, then contract sharply. This constant churn, the overhead of connection creation (averaging 47ms), and the subsequent database vacuuming during these scale-up events introduced its own set of performance problems. It solved the exhaustion problem, but introduced a new kind of volatility.

Sharding the Load: Isolation Prevents Cascades

The Partitioned Pool, creating separate pools for each database shard, offered a more sophisticated isolation strategy:

// Pool per shard
Map<ShardId, HikariDataSource> pools;
for (ShardId shard : shards) {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(25);
config.setMinimumIdle(15);
pools.put(shard, new HikariDataSource(config));
}

Results:

  • Throughput: 5,621 req/sec (97% better than baseline)
  • P99 latency: 1,287ms (85% better)
  • Connection wait time: P99 = 230ms
  • Pool exhaustion events: 183
  • Failed requests: 1.8%

This was a significant leap forward. By isolating shards, a surge on one didn’t impact others. The cascade failures we’d seen with simpler models were averted. The downside? We saw an overall connection count of 400 across all shards, pushing the limits of our database’s configured connection count. Overprovisioning became a new concern.

Prioritizing What Matters: Business-Critical Speed

Then came the Priority Queue Pool. This strategy, where critical requests jump the queue, yielded a fascinating outcome:

class PriorityPool extends HikariDataSource {\n    PriorityBlockingQueue<ConnectionRequest> queue;\n    Connection getConnection(Priority priority) {\n        ConnectionRequest req = new ConnectionRequest(priority);\n        queue.offer(req);\n        return req.await();\n    }
}

Results:

  • Throughput: 3,421 req/sec (20% better than baseline overall)
  • P50 latency: 203ms (overall)
  • Critical path P99: 387ms (96% better!)
  • Connection wait time: Critical P99 = 45ms
  • Failed critical requests: 0.3%

This was the “aha!” moment. While overall throughput wasn’t the absolute highest, the business-critical path — checkout, payments — was now dramatically faster. This strategy doesn’t necessarily boost raw transactions per second as much as it guarantees performance for your most important operations. It’s about user experience for the tasks that actually drive revenue.

What About the Others?

Connection Borrowing felt like a band-aid. It offered marginal gains but introduced significant complexity in tracking and ensuring connections were returned, leading to potential leaks under stress. It’s a fine-tuning knob, not a foundational strategy.

Pre-warmed Pools showed promise, reducing initial connection latency during traffic surges by having connections ready. However, this came at the cost of maintaining idle connections constantly, potentially wasting resources during quiet periods. It’s best as a component of a more adaptive strategy.

The Winner? It Depends.

So, which strategy reigns supreme? The answer, as always, is nuanced, and frankly, it’s a bit of a cop-out to say “it depends.” But in this case, it’s the truth dictated by the data. The Hybrid Adaptive pool, which we didn’t detail here due to space constraints but which intelligently combined elastic sizing with priority queuing, generally performed the best across multiple metrics. It managed to achieve high throughput while ensuring critical operations remained responsive, without the wild resource swings of a purely elastic model.

However, the 312% difference isn’t just about finding the absolute best. It’s about recognizing that your default is almost certainly leaving performance on the table. For a high-traffic e-commerce site with critical checkout paths, the Priority Queue Pool or a Hybrid Adaptive approach would be the clear favorites. For a data-analytics platform where consistent read performance is key, a well-tuned Partitioned Pool might be ideal. If your workload is extremely predictable and low-volume, a Naive Fixed Pool might suffice, but you’re likely losing out on potential scalability.

The takeaway isn’t just about tuning a single parameter. It’s about understanding your application’s traffic patterns, identifying your critical user journeys, and choosing a pooling strategy that actively supports, rather than hinders, those goals. Don’t just set it and forget it; benchmark it and optimize it.


🧬 Related Insights

Frequently Asked Questions

What does connection pooling actually do? Connection pooling maintains a set of database connections that applications can reuse. Instead of creating a new connection each time it’s needed (which is slow and resource-intensive), the application borrows an existing connection from the pool and returns it when done.

Will changing my connection pool strategy fix my slow application? It can significantly improve performance if connection pool exhaustion or contention is a bottleneck. However, if your application is slow due to inefficient queries, application-level logic, or other infrastructure issues, changing the connection pool alone won’t solve those problems.

Is HikariCP bad because of these results? No, HikariCP is widely regarded as one of the fastest and most performant JDBC connection pool implementations available. The results here highlight that even the best tools require proper configuration and tuning based on specific workload demands, not just relying on default settings.

Written by
DevTools Feed Editorial Team

Curated insights and analysis from the editorial team.

Frequently asked questions

What does connection pooling actually do?
Connection pooling maintains a set of database connections that applications can reuse. Instead of creating a new connection each time it's needed (which is slow and resource-intensive), the application borrows an existing connection from the pool and returns it when done.
Will changing my connection pool strategy fix my slow application?
It can significantly improve performance if connection pool exhaustion or contention is a bottleneck. However, if your application is slow due to inefficient queries, application-level logic, or other infrastructure issues, changing the connection pool alone won't solve those problems.
Is HikariCP bad because of these results?
No, HikariCP is widely regarded as one of the fastest and most performant JDBC connection pool implementations available. The results here highlight that even the best tools require proper configuration and tuning based on specific workload demands, not just relying on default settings.

Worth sharing?

Get the best Developer Tools stories of the week in your inbox — no noise, no spam.

Originally reported by dev.to

Stay in the loop

The week's most important stories from DevTools Feed, delivered once a week.