Configuring the HikariCP Leak Detection Threshold
This guide is part of HikariCP Configuration Deep Dive. It answers one question precisely: which code path is holding a connection open far longer than it should? HikariCP will not find that for you by default. The leakDetectionThreshold property turns it on, and the moment it fires you get the stack trace of the exact borrow site — the single most useful artifact in a pool exhaustion investigation.
The property is disabled out of the box, and that default causes real pain. A service that slowly drifts toward SQLTransientConnectionException: Connection is not available, request timed out after 30000ms gives you a timeout on the victim thread — a thread that did nothing wrong — while the thread actually holding the connection is invisible. Leak detection inverts that: it names the culprit.
What the property actually does
Every time a thread borrows a connection, HikariCP schedules a one-shot task on its internal housekeeping executor, delayed by leakDetectionThreshold milliseconds. If the connection is returned before the task fires, the task is cancelled and nothing is logged. If the connection is still checked out when the task fires, HikariCP logs a WARN from ProxyLeakTask containing the stack trace captured at borrow time.
Two consequences follow from that mechanism, and both are routinely misunderstood:
- It never closes or reclaims the connection. Detection is pure observation. The connection stays checked out; the pool stays one connection smaller. If you want a connection forcibly recycled you need
maxLifetime, which is covered in tuning HikariCP maxLifetime and idleTimeout. - A warning is not proof of a bug. It proves only that a borrow exceeded the threshold. A legitimately slow report query will trip a 30-second threshold every time it runs. The stack trace tells you which it is.
Choosing a threshold
The useful rule is: set it above your slowest legitimate borrow and below your acquisition timeout. Below the slowest legitimate borrow and the log fills with false positives you learn to ignore. Above connectionTimeout and the pool has already started failing requests before the warning appears.
| Workload | Slowest legitimate hold | Suggested leakDetectionThreshold |
Reasoning |
|---|---|---|---|
| Web request handlers only | under 1 s | 5000 |
Anything over five seconds in a request path is pathological. |
| Mixed web plus light batch | 5–10 s | 20000 |
Tolerates the batch tail while still landing inside a 30 s timeout. |
| Reporting or ETL service | 60 s+ | 120000 |
Long holds are the design; only truly stuck borrows should trip. |
| Diagnosing an active incident | any | 2000 |
Deliberately noisy. You want every long borrow named, then revert. |
HikariCP enforces a floor: values below 2000 ms are rejected and the feature is treated as disabled, with a warning at startup. Zero disables detection, which is the default.
A practical starting point for an ordinary service is leakDetectionThreshold a little under half of connectionTimeout. With the default 30-second timeout that puts you around 10–15 seconds: long enough that normal traffic is silent, early enough that you see the warning before the first victim thread times out.
Configuration by framework
Spring Boot exposes the property through the spring.datasource.hikari prefix, so nothing custom is required:
spring:
datasource:
hikari:
pool-name: orders-pool
maximum-pool-size: 20
connection-timeout: 30000 # borrow gives up here
leak-detection-threshold: 12000 # warn well before that
max-lifetime: 1740000
idle-timeout: 600000
The pool-name matters more than it looks. The warning is logged by the pool’s own logger, and without a distinct name a service with several data sources gives you warnings you cannot attribute. Full data source setup is covered in Spring Boot DataSource Configuration.
For plain Java, set it on the config object before constructing the pool:
HikariConfig config = new HikariConfig();
config.setPoolName("orders-pool");
config.setJdbcUrl("jdbc:postgresql://db.internal:5432/orders");
config.setMaximumPoolSize(20);
config.setConnectionTimeout(30_000);
config.setLeakDetectionThreshold(12_000);
HikariDataSource ds = new HikariDataSource(config);
The property is also settable at runtime through the HikariPoolMXBean JMX interface, which is the safest way to turn it on mid-incident:
// via JMX: com.zaxxer.hikari:type=PoolConfig (orders-pool)
poolConfigMBean.setLeakDetectionThreshold(2000);
Make sure the logger is not filtered out, or you will enable the feature and see nothing:
logging:
level:
com.zaxxer.hikari.pool.ProxyLeakTask: WARN
com.zaxxer.hikari.pool.HikariPool: WARN
Reading the warning
A fired detection looks like this:
WARN c.z.h.pool.ProxyLeakTask - Connection leak detection triggered for
org.postgresql.jdbc.PgConnection@5f2b1c3e on thread http-nio-8080-exec-7,
stack trace follows
java.lang.Exception: Apparent connection leak detected
at com.example.report.LedgerExporter.streamRows(LedgerExporter.java:88)
at com.example.report.ReportService.export(ReportService.java:41)
at com.example.web.ReportController.download(ReportController.java:29)
Read it from the bottom up. The frame nearest the bottom of your own code is the entry point; the frame nearest the top — here LedgerExporter.streamRows — is the site that took the connection. That is where to look for a missing close(), a try block without try-with-resources, or a stream handed to a caller who never drains it.
HikariCP 4.0 and later also emit a follow-up when the connection eventually comes back:
INFO c.z.h.pool.ProxyLeakTask - Previously reported leaked connection
org.postgresql.jdbc.PgConnection@5f2b1c3e on thread http-nio-8080-exec-7 was returned
That second line is the discriminator. A warning followed by a “was returned” message is a slow borrow — the connection came back, just late. A warning with no matching return is a genuine leak: the connection is gone until the JVM restarts or maxLifetime retires it. Grep for warnings whose object identity never appears in a return line and you have your true leak list.
Common failure patterns and remediation
| Symptom | Underlying cause | Remediation |
|---|---|---|
| Property set but nothing ever logged | Value below the 2000 ms floor, or the com.zaxxer.hikari logger filtered above WARN |
Raise to at least 2000 and set the logger level explicitly |
| Warnings on every request | Threshold set inside normal hold time | Measure p99 borrow duration from pool metrics and set above it |
| Warning fires, connection never returns | Real leak — connection escaped a scope without close() |
Fix the borrow site from the stack trace; use try-with-resources |
| Warning fires, “was returned” follows seconds later | Slow query or slow downstream call while holding the connection | Move the slow work outside the borrow, or shorten the query |
| Warnings only from one pool in a multi-source app | Correct behaviour, but unattributable without names | Set a distinct poolName per data source |
| Warnings vanish after a deploy but exhaustion continues | Leak moved to a path where holds are shorter than the threshold | Temporarily drop the threshold to 2000 and re-observe |
| Stack trace shows only framework frames | The borrow happens inside an ORM or proxy layer | Trace the caller through ORM lifecycle hooks rather than the trace alone |
What it costs to leave enabled
The overhead is one scheduled task per borrow plus one Exception allocation to capture the stack. The exception is constructed at borrow time whether or not the timer ever fires, and filling in a stack trace is the expensive part — roughly a few microseconds on modern JVMs. Against a borrow that will hold a socket for milliseconds, that is negligible for almost every service.
The honest exception is a very hot pool doing tens of thousands of extremely short borrows per second, where stack capture becomes measurable against total borrow cost. If you are in that regime, benchmark it rather than guessing; the methodology in Java Connection Pool Benchmarks applies directly. For everyone else, leaving detection on permanently is the right default — the cost is small and the alternative is discovering leaks only through outages.
Operational boundary
Leak detection tells you where a connection was taken. It does not tell you whether taking it that long was wrong, it does not free the connection, and it cannot see a connection held by a thread that has since died holding it. When warnings point at code you know is legitimately slow, the answer is not a higher threshold — it is a separate data source for the slow work, so long holds cannot starve request traffic. Sizing those two pools independently is covered in Connection Pool Sizing Formulas.
Frequently Asked Questions
Does leak detection close the leaked connection?
maxLifetime retires it after it is finally returned. If you need bounded recovery from stuck connections, that is a maxLifetime and query-timeout concern, not a detection concern.What is the minimum value I can set?
Can I change it without a restart?
leakDetectionThreshold is one of the properties exposed through HikariCP’s JMX HikariConfigMXBean, so you can enable or tighten it on a running JVM during an incident and revert afterwards.Why does the stack trace point at framework code instead of mine?
Should I alert on these warnings?
Is it safe to run in production permanently?
Related
- HikariCP Configuration Deep Dive — the parent guide covering the full parameter set.
- Tuning HikariCP maxLifetime and idleTimeout — the properties that actually retire connections.
- Connection Acquisition Timeout Strategies — the timeout your threshold must sit below.
- Spring Boot DataSource Configuration — where these properties live in a Boot application.
- Detecting Connection Pool Saturation — the metric-side view of the same failure.