Databases 24 min read

HikariCP Connection Pool: Wait Timeout, Leak Detection & Sizing Strategies

This article analyzes HikariCP connection pool timeout and leak issues through a production incident, explaining connectionTimeout behavior, leakDetectionThreshold mechanics, pool sizing formulas, transaction scope impact on connection hold time, maxLifetime and keepaliveTime configuration for network infrastructure, and practical troubleshooting using thread dumps and pool metrics.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
HikariCP Connection Pool: Wait Timeout, Leak Detection & Sizing Strategies

The article opens with a production incident: at 8:07 PM, order service error rates spiked. Logs showed no slow SQL alerts, database CPU was under 40%, network was normal, but connection timeouts flooded the logs:

2026-10-14 20:41:07.228 ERROR [http-nio-8080-exec-187] c.e.order.OrderService : 查询订单失败 orderNo=2026101400871
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms (total=10, active=10, idle=0, waiting=37)

The exception reveals four key metrics: total=10 (total connections in pool), active=10 (borrowed and not returned), idle=0 (available connections), waiting=37 (threads queued). The pool is exhausted with no connections returning.

Thread Dump Analysis

A thread dump shows dozens of HTTP worker threads stuck at the same stack frame:

"http-nio-8080-exec-142" #142 daemon prio=5 os_prio=0 tid=0x0000023f8c1a2000 nid=0x5e14 waiting on condition [0x000000c1a2efe000]
  java.lang.Thread.State: WAITING (parking)
   at sun.misc.Unsafe.park(Native Method)
   - parking to wait for  <0x000000076b2c8f10> (a java.util.concurrent.SynchronousQueue$TransferStack)
   at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
   at com.zaxxer.hikari.util.ConcurrentBag.borrow(ConcurrentBag.java:154)
   at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:177)
   at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:162)
   at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128)
   at com.example.order.repository.OrderRepository.findByNo(OrderRepository.java:64)

The top frame ConcurrentBag.borrow is HikariCP's sole connection-borrowing entry point. Two frames down reaches business DAO code. This confirms threads are blocked waiting for connections; the root cause of non-returning connections requires further investigation.

Diagnostic Decision Tree from Pool Metrics

The author outlines a fixed diagnostic direction based on the four metrics:

active ≈ total, waiting rising → enable leak-detection-threshold to check for leak stacks; if none, investigate transaction scope and slow queries.

total < maximum-pool-size → pool cannot reach database; check database max_connections, credentials, network.

waiting stays 0 but APIs are slow → problem is not in the pool; investigate SQL performance.

connectionTimeout: Wait Location and Value

ConcurrentBag.borrow

is the only entry for borrowing connections. If idle connections exist, it takes one immediately. Otherwise it checks if current connections reached maximumPoolSize; if not, a creator thread builds a new connection; if yes, the thread enters a wait queue. When a connection is returned, the pool hands it directly to the queue head — no timed polling. Only after exceeding connectionTimeout with no return does it throw SQLTransientConnectionException.

Default is 30,000 ms; minimum allowed is 250 ms. The default 30 seconds is too long for web apps. If upstream gateway timeout is 5 seconds, users fail at 5s while application threads wait until 30s, occupying threads, request contexts, and skewing connection stats. Reduce connectionTimeout to near upstream timeout so failures surface early, freeing threads. Error count will rise initially — this is normal: previously requests hung 30s before erroring; now they fast-fail at 3s. Total errors unchanged, just shifted from "hanging" to "fast fail". Fast fail also lets health checks evict instances sooner.

Spring Boot Configuration Example

Spring Boot uses spring.datasource.hikari prefix. Production config shown:

spring:
  datasource:
    url: jdbc:mysql://10.20.3.11:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: order_app
    password: ${DB_PASSWORD}
    hikari:
      pool-name: order-pool
      maximum-pool-size: 16
      minimum-idle: 16
      connection-timeout: 3000
      validation-timeout: 2000
      max-lifetime: 1740000
      keepalive-time: 60000
      leak-detection-threshold: 30000
pool-name

should be set explicitly; default names like HikariPool-1, HikariPool-2 make it impossible to identify which pool logs belong to when multiple data sources exist.

Programmatic configuration via HikariConfig setters maps 1:1 to YAML keys; all accept milliseconds — do not pass seconds directly.

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://10.20.3.11:3306/order_db");
config.setUsername("order_app");
config.setPassword(System.getenv("DB_PASSWORD"));
config.setPoolName("order-pool");
config.setMaximumPoolSize(16);
config.setMinimumIdle(16);
config.setConnectionTimeout(3_000);
config.setMaxLifetime(1_740_000);
config.setKeepaliveTime(60_000);
config.setLeakDetectionThreshold(30_000);

HikariDataSource dataSource = new HikariDataSource(config);

Pool Sizing and minimumIdle Rationale

maximum-pool-size

defaults to 10; minimum-idle defaults to same, making pool fixed-size by default. Fixed size avoids creating connections during peak traffic — TCP handshake, auth, TLS add tens of milliseconds at the worst moment.

Cost: low-traffic periods still hold all connections. 20 pods × 10 connections = 200 connections on database, plus batch jobs, ops tools, other services — easily hits max_connections. Capacity planning must use "instances × per-instance pool max", not single-service config.

HikariCP official heuristic: connections = CPU cores × 2 + effective disk count. For 8-core DB with SSD → 17 connections. Seems low, but DB parallelism is CPU-bound; extra connections just contend locks, context-switch, slow each other. Cases exist where raising pool from 10 to 100 worsened problems: DB CPU saturated, disk queue grew, single query latency from 15ms to 400ms, all 100 connections occupied, wait queue longer than before.

Correct sequence: confirm no leaks, no long transactions, no slow queries; then check if wait time acceptable; only then increase connections, leaving headroom for DB connection quota. minimum-idle < maximum-pool-size enables scaling: pool reclaims idle connections per idleTimeout (default 10 min). This parameter only matters in scaling mode; in fixed pool changing it has zero effect — often overlooked when debugging "why idle connections not reclaimed".

Transaction Scope and Connection Hold Time

Pool size fixed; throughput depends on how long each connection is held. @Transactional methods borrow connection at entry, return after commit/rollback. All intermediate code holds the connection: remote calls, file I/O, lock waits.

Example: createOrder calls payment gateway (avg 800ms, peak 5s) inside transaction — connection held uselessly during gateway call.

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private PaymentGateway paymentGateway;

    @Transactional
    public void createOrder(CreateOrderCmd cmd) {
        Order order = Order.from(cmd);
        orderRepository.save(order);
        // Payment gateway avg 800ms, peak 5s — connection held idle
        String payNo = paymentGateway.charge(order.getAmount());
        orderRepository.updatePayNo(order.getId(), payNo);
    }
}

Math: 10 connections, 50ms hold → 200 ops/sec theoretical max. Hold time 500ms → 20 ops/sec, 10× drop. One remote call can push hold time from ms to seconds, dropping throughput two orders of magnitude — no error logs, just growing connection timeouts.

Fix: move remote call outside transaction. Cost: intermediate unpaid state must be persisted.

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private PaymentGateway paymentGateway;

    public void createOrder(CreateOrderCmd cmd) {
        // First short transaction: local persist only, ms hold time
        Order order = orderRepository.save(Order.from(cmd));
        // Gateway call outside transaction — no connection held
        String payNo = paymentGateway.charge(order.getAmount());
        // Second short transaction: write back payment reference
        orderRepository.updatePayState(order.getId(), payNo, OrderState.PAID);
    }
}

Intermediate state requires reconciliation/compensation jobs if gateway succeeds but write-back fails. Decision depends on gateway latency distribution: stable <100ms → keep in transaction; tail latency seconds → split.

Slow queries are another hidden source. One 3-second SQL on 10-connection pool caps that path at 3.3 concurrent ops regardless of Tomcat threads. Slow query log entries of several seconds and connection pool timeouts are often two symptoms of same problem. Batch inserts: 10,000 inserts in one transaction holds connection start-to-finish with row locks and undo; batch commit every 500 rows reduces hold time to 1/20th.

Leak Detection: Criteria and False Positives

Long-held connections vs permanently leaked connections look similar but differ: former eventually return; latter never return, available connections drain to zero. leakDetectionThreshold (default 0 = off) distinguishes them. When enabled, each borrowed connection gets a delayed task; at threshold, checks if returned; if not, logs warning with full borrow-time stack trace; task cancelled on return. Threshold has floor: <2000ms ignored and detection disabled. Reason: detection maintains delayed tasks; too-short threshold hits normal queries, floods logs with noise, drowning real leaks.

Config example:

spring:
  datasource:
    hikari:
      leak-detection-threshold: 30000
      maximum-pool-size: 16

Log format:

2026-10-14 21:03:12.881 WARN  [http-nio-8080-exec-52] com.zaxxer.hikari.proxy.ProxyLeakTask : Connection leak detection triggered for com.mysql.cj.jdbc.ConnectionImpl@5f2a91c3 on thread http-nio-8080-exec-52, stack trace follows
java.lang.Exception: Apparent connection leak detected
	at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128)
	at com.example.report.ReportExportService.exportDaily(ReportExportService.java:87)
	at com.example.report.ReportController.download(ReportController.java:41)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

Stack points to ReportExportService.exportDaily line 87 — the leak source.

Key word: Apparent . It detects "held longer than threshold", not "never returned". A 40-second report query with 30-second threshold alarms every run despite being normal. Noise trains teams to ignore the log, rendering detection useless.

Fix: set threshold above slowest legitimate operation (e.g., report export 40s → threshold 120s). Add separate monitoring for slow paths so they don't pollute pool alerts. HikariCP has no per-call-source exclusion; only threshold is tunable.

Real leaks typically come from: manual getConnection() with close() only on happy path — exception skips close; storing Connection in instance/static fields for reuse but never returning; MyBatis manual openSession() without close.

// Leak pattern: exception path skips close
public void syncChannelOrder(String bizNo) {
    Connection conn = dataSource.getConnection();
    PreparedStatement ps = conn.prepareStatement("update t_channel_order set state = ? where biz_no = ?");
    ps.setInt(1, 2);
    ps.setString(2, bizNo);
    ps.executeUpdate();
    conn.close();
}

Rewrite with try-with-resources so JVM closes connection, statement, result set on all paths including exceptions.

public void syncChannelOrder(String bizNo) throws SQLException {
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(
                 "update t_channel_order set state = ? where biz_no = ?")) {
        ps.setInt(1, 2);
        ps.setString(2, bizNo);
        ps.executeUpdate();
    }
}

Spring-managed transactions need no manual handling — JdbcTemplate, MyBatis, JPA return connections at transaction end. High-risk zone is manual getConnection() from DataSource mixed with business logic.

maxLifetime and Connection Validity

maxLifetime

default 1,800,000 ms (30 min) governs total connection lifespan from creation to eviction, regardless of busy/idle. Expiring connections are not interrupted mid-use; HikariCP waits for current use to finish, then closes and removes on return. Thus maxLifetime is a soft upper bound; actual life may exceed config slightly. HikariCP adds random offset to avoid mass expiry/reconnect storms if all connections created at startup.

Must align with all idle-recycling layers. MySQL wait_timeout default 28,800s (8h) seems generous, but load balancers, NAT gateways, firewalls between app and DB have much shorter idle timeouts: 350s, 900s, even 60s seen.

When middle device silently kills connection, both sides unaware. HikariCP thinks connection healthy; next borrower sends request, discovers broken socket: Communications link failure or Connection reset. Errors are low-frequency, clustered at specific times, disappear after restart. Hard to link to pool config from app logs alone; often discovered after multiple deployments reproduce.

Two solutions: (1) set maxLifetime shorter than shortest idle timeout in chain, letting HikariCP proactively rotate connections. (2) enable keepaliveTime (default 0 = off) — pool periodically pings idle connections to keep them alive in middle devices' view.

spring:
  datasource:
    hikari:
      max-lifetime: 1740000   # 29 min, earlier than 30-min chain timeout
      keepalive-time: 60000   # ping every minute, must be < max-lifetime
keepaliveTime

must be < maxLifetime; otherwise connection evicted before keepalive runs, rendering config useless.

Overlooked issue: pool stats logs are DEBUG level by default. Without enabling com.zaxxer.hikari DEBUG or integrating Micrometer, incident logs show only exceptions, not total/active/idle/waiting metrics. Key Micrometer metrics: hikaricp.connections.active, hikaricp.connections.pending, hikaricp.connections.timeout. Plot together; trend more useful than single snapshot. pending > 0 persistently is early pool-pressure signal, earlier than exceptions.

Conclusion: connection pool problems reduce to two causes — connections held too long, or connections never returned. Former: investigate transaction scope and slow queries. Latter: enable leak detection, examine stacks. Increasing pool size only delays explosion; larger pool increases DB concurrent pressure, moving problem elsewhere.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

connection pooltroubleshootingHikariCPtransaction managementkeepaliveTimeconnectionTimeoutleakDetectionThresholdmaximumPoolSizemaxLifetime
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.