HikariCP — Connection is not available, request timed out after 30000ms
Diagnose HikariCP connection pool exhaustion, read active and pending counts, find connection-holding code (missing close, long transactions), and size the pool correctly.
Symptoms
- java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms
- Logs show thousands of threads in WAITING on HikariPool.getConnection while a few threads hold connections
- The app recovers instantly when load stops but degrades continuously under sustained traffic
Possible causes
- A code path obtains a connection and leaks it (missing try-with-resources or early returns before close)
- Long transactions hold connections while doing slow work: HTTP calls, file IO, or sleeps inside the transaction
- Pool size is smaller than real concurrency, and request handling waits on connection acquisition
- A prepared-statement or result-set leak keeps the underlying connection checked out
Diagnosis steps
Read the pool metrics
curl -s localhost:8080/actuator/metrics/hikaricp.connections.active; curl -s localhost:8080/actuator/metrics/hikaricp.connections.pendingInterpretation
Active pinned at maximum plus pending growing by the second is exhaustion; active staying below max while timing out points to leak detection or connection-level errors.
Find the holders
jstack <pid> | grep -B5 "HikariPool.getConnection" | head -30; or enable leak detection: spring.datasource.hikari.leak-detection-threshold: 60000Interpretation
Leak detection logs a stack trace for connections held over the threshold — the exact code path that never returns them.
Audit transaction boundaries
grep -Rn "@Transactional" src/ | head -20Interpretation
Transactional methods doing remote calls hold pool connections for the whole call duration; each slow downstream shrinks the pool.
Root cause
The pool hands out a fixed number of connections. Exhaustion means holders retain connections longer than the arrival rate of requests: a leak removes some permanently, and long transactions multiply the holding time per request. New requests then queue for the configured connectionTimeout and fail with SQLTransientConnectionException.
Fix
Close resources deterministically with try-with-resources, move slow work out of transaction boundaries, size the pool from connection-time budget rather than thread count, enable leak detection in staging, and make downstream calls resilient with timeouts shorter than the connection timeout.
Risk note
Oversizing the pool shifts the queue to the database, exhausting its connection limit and hurting every other service. Size from measured holding times.
Verify the fix
Under peak load, active connections stay below the max, pending stays near zero, and no acquisition timeout appears in logs over a full soak.
Symptom walkthrough
The exception text contains the configured budget and the pool name:
java.sql.SQLTransientConnectionException:
HikariPool-1 - Connection is not available, request timed out
after 30000ms.
The metrics pair tells you which failure mode you have:
hikaricp.connections.active = 10 <- pinned at max
hikaricp.connections.pending = 214 <- growing every second
Active at maximum with pending climbing is classic saturation. The decisive tool is leak detection with a threshold — it prints the acquisition stack of every connection held longer than the threshold, converting a mystery into a file and line:
Fix in practice
Close deterministically; the pattern is non-negotiable:
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
return map(rs);
} // all three closed on every path
Shrink transaction scope — the pool is a connection-time budget, not a thread budget:
public Order place(OrderRequest req) {
var quote = pricingClient.quote(req); // HTTP call: OUTSIDE transaction
return tx.run(() -> orderDao.insert(quote.toOrder())); // milliseconds
}
Enable leak detection outside production, and watch the wait ladder:
spring:
datasource:
hikari:
leak-detection-threshold: 60000
maximum-pool-size: 12 # from measured hold-time, not cores
And bound every downstream call to less than connectionTimeout, so a slow dependency degrades its own requests instead of the whole pool.