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.

15 апреля 2025 г.8 мин чтения
PostgreSQLHikariCPJava

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:

  • External I/O inside transactions — If your code calls an external API while holding a DB connection, average_hold_time becomes: db_time + external_api_time. A 200ms payment API call inside @Transactional means each request holds a connection 10× longer than expected.
  • Batch operations — A single batch job that holds a connection for 10 seconds while processing records changes the math entirely.
  • Burst traffic — Little's Law describes steady-state. During a 10× traffic spike lasting 30 seconds, you need headroom.
  • 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

    SituationFormulaTypical Result
    |-----------|---------|----------------|
    I/O-bound API(cores × 2) + spindles9-20
    CPU-bound processingcores + 15-9
    High-traffic serviceRPS × avg_hold_timeDepends
    With external I/O in transactionFix the code firstN/A
    The most important rule: measure actual connection hold time in production before sizing. Use pg_stat_activity and HikariCP metrics together.

    Practice This

    Want to see connection pool exhaustion in action? Try the interactive case study:

    Хочешь попрактиковаться?
    Примени знания в интерактивном кейсе
    Смотреть кейсы →← Все статьи