When an API starts throwing connection timeout exceptions during traffic spikes, the initial reaction of many developers is simple: "Increase the maximum pool size!"
They bump maximumPoolSize from 20 to 100, then to 200. Suddenly, database CPU usage spikes to 100%, disk I/O thrashes, and response times degrade from 15ms to 3 seconds.
Why does adding database connections hurt performance?
In this article, we explore the physics of database connection pooling, examine why small pools outperform large pools, and learn how to tune HikariCP (the default connection pool in Spring Boot) for peak throughput.
The Fundamental Pool Formula: Less Is More
A database server is governed by hardware limits: the number of physical CPU cores, disk I/O throughput, and network interface capacity.
When 200 connections simultaneously execute queries on an 8-core CPU server:
- Only 8 queries can actually run at any given nanosecond.
- The remaining 192 connections are actively competing for CPU time.
- The OS kernel spends more time context switching between threads and thrashing CPU cache lines than executing actual SQL statements!
PostgreSQL and HikariCP engineers established the famous empirical rule for pool sizing:
$$\text{Connections} = (\text{Core Count} \times 2) + \text{Effective Spindle Count}$$
For a modern server with 8 CPU cores and fast NVMe SSD storage:
$$\text{Connections} = (8 \times 2) + 1 = 17 \text{ connections!}$$
A pool of 15 to 25 connections will routinely achieve higher transactions-per-second than a pool of 200 connections on the same hardware.
Critical HikariCP Configuration Parameters
Here is an optimized production configuration in application.yml:
spring:
datasource:
hikari:
pool-name: ProductionHikariCP
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 30000 # 30 seconds
idle-timeout: 600000 # 10 minutes
max-lifetime: 1800000 # 30 minutes
leak-detection-threshold: 2000 # 2 seconds
auto-commit: true
Key Settings:
-
Fixed Pool Size (
maximum-pool-size == minimum-idle): Prevents runtime latency spikes from dynamically establishing new TCP handshakes during traffic surges. -
max-lifetime(30 mins): Retires connections gracefully before cloud NAT gateways or firewalls drop stale TCP connections. -
leak-detection-threshold(2000ms): Logs a stack trace warning whenever a thread holds a connection for longer than 2 seconds, immediately identifying unclosed connections.
Key Production Takeaways
- Keep pools small: Measure query throughput while gradually lowering your pool size. You will often find lower latency and higher QPS with 20 connections than 100.
-
Never perform external I/O inside
@Transactional: Never invoke external REST APIs or write to disk while holding a database connection. Check out the connection as late as possible and return it immediately. -
Turn on Leak Detection: Set
leak-detection-threshold = 2000in staging and production to catch connection retention bugs early.

Top comments (0)