How to Size Your Connection Pool: The Math Behind HikariCP
Most teams set maxPoolSize=10 and forget it. Here's the formula that actually works, derived from Little's Law and real production incidents.
The Problem with Guessing
Most engineering teams pick a connection pool size by intuition — "10 feels right" or "let's try 50 and see what happens." This is how you end up with either starved threads or exhausted databases.
There's a better way. It's called Little's Law, and it comes from queueing theory.
Little's Law Applied to Connection Pools
The fundamental formula:
``
pool_size = throughput × average_hold_time
`
Where:
- throughput = requests per second that need a DB connection
- average_hold_time = seconds each request holds a connection
Example Calculation
Your API handles 500 RPS. Each request makes one DB call averaging 20ms:
`
pool_size = 500 RPS × 0.020s = 10 connections
`
That's why the HikariCP default of 10 "works" for many small services. It's not magic — it's math.
The HikariCP Formula
HikariCP's own documentation recommends a different formula for PostgreSQL:
`
pool_size = (core_count × 2) + effective_spindle_count
`
For a 4-core server with SSD storage (spindle_count ≈ 1):
`
pool_size = (4 × 2) + 1 = 9
`
This formula accounts for I/O wait — connections block on disk I/O, not CPU. So you can handle more concurrent connections than CPU cores.
When the Formula Breaks Down
The formulas above assume a uniform request pattern. In reality:
Practical Configuration
`yaml
spring.datasource.hikari:
# Base: Little's Law result
maximum-pool-size: 20
# Minimum idle connections (warm pool, reduce latency) minimum-idle: 5
# Timeout before throwing "connection not available" connection-timeout: 3000 # 3s, not 30s!
# Max time a connection can be idle before eviction idle-timeout: 300000 # 5 minutes
# Rotate connections to prevent stale state
max-lifetime: 1800000 # 30 minutes
`
Why 3s connection-timeout?
The default 30s timeout means requests queue for 30 seconds before failing. During an outage, you get a 30-second cascade where requests pile up, memory fills, and the service dies completely.
With 3s, requests fail fast. Your load balancer routes them elsewhere. The system stays partial (some requests fail) rather than total (all requests eventually fail).
Database-Side Limits
Connection pool sizing must account for the database's own limits:
`sql
-- PostgreSQL: check current and max connections
SELECT count(*) FROM pg_stat_activity;
SHOW max_connections;
-- Rule: total pool size across ALL app instances
-- must be < max_connections - 10 (leave room for admin/monitoring)
`
For a service with 5 replicas, each with pool_size=20:
- Total connections: 5 × 20 = 100
- PostgreSQL default max_connections: 100
- That's exactly at the limit — add one replica and you're in trouble
Monitoring Your Pool
Add these metrics to Grafana:
`yaml
# Micrometer metrics exposed by HikariCP
hikaricp.connections.active # currently in use
hikaricp.connections.idle # waiting in pool
hikaricp.connections.pending # waiting for a connection (danger signal)
hikaricp.connections.timeout # failed acquisitions (critical)
`
Alert when:
- connections.pending > 0
for more than 10 seconds - connections.timeout > 0
(any timeout is a problem) - connections.active / maximum-pool-size > 0.8` (running at >80% capacity)
Summary
| Situation | Formula | Typical Result |
| I/O-bound API | (cores × 2) + spindles | 9-20 |
| CPU-bound processing | cores + 1 | 5-9 |
| High-traffic service | RPS × avg_hold_time | Depends |
| With external I/O in transaction | Fix the code first | N/A |
Practice This
Want to see connection pool exhaustion in action? Try the interactive case study:
→ [DB-005: PostgreSQL Connection Pool Exhaustion](/cases/database/postgresql/db-005-connection-pool) — The same pattern described here, with real logs and a live investigation environment.