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.
When the leak detection task fires Two borrow timelines. In the first, the connection is returned before the threshold elapses and the scheduled task is cancelled silently. In the second, the connection is still held when the threshold elapses, so a warning with the borrow-time stack trace is logged. One timer is scheduled per borrow and cancelled on return healthy borrow connection held 400 ms close() — timer cancelled, nothing logged suspect borrow connection still held past the threshold threshold elapses — WARN with borrow stack connection is NOT reclaimed The stack trace in the warning is captured at borrow time, not at the moment the timer fires. That is why it points at the code that took the connection rather than at the housekeeping thread.
Detection is a cancellable timer per borrow. Returning the connection in time cancels it; holding past the threshold logs the borrow-site stack trace without reclaiming anything.

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
Placing the threshold on the borrow-duration axis A duration axis showing the band of normal borrows, the leak detection threshold placed above it, and the connection timeout above that. Setting the threshold inside the normal band produces false positives; setting it above the timeout produces warnings that arrive too late. The threshold belongs between the slowest normal borrow and the timeout 0 s borrow duration normal borrows live here leakDetectionThreshold connectionTimeout threshold here — constant false positives threshold here — warning arrives with time to act threshold beyond the timeout — requests already failing before you are told
Place the threshold in the gap between routine hold time and the acquisition timeout. Both ends of that gap are failure modes.

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.

From warning to remediation A decision flow. Starting from a logged warning, check whether a matching return message appeared. If it did the borrow was merely slow and the work should move outside the borrow. If it did not, the connection leaked and the borrow site needs a close. warning logged for a connection did a matching return message follow? yes no slow borrow, not a leak move slow work outside the borrow or give it its own data source genuine leak fix the top frame of the borrow stack wrap in try-with-resources The return message is the only signal that separates the two cases, and it is the reason to keep INFO logging enabled for the pool package rather than filtering it down to WARN alone.
The presence or absence of a matching return message splits every warning into the two cases that need different fixes.

Frequently Asked Questions

Does leak detection close the leaked connection?
No. It only logs. The connection remains checked out and unusable until the owning code returns it or 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?
2000 milliseconds. HikariCP rejects anything lower and treats the feature as disabled, logging a startup warning. Zero, the default, means disabled.
Can I change it without a restart?
Yes. 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?
Because the borrow happened inside that framework. With an ORM or a transaction proxy, the pool sees the proxy as the borrower. Work outward through the trace to the first frame in your own package, and cross-reference with ORM Connection Lifecycle Hooks.
Should I alert on these warnings?
Alert on the rate, not on individual events. A single warning during a nightly batch is noise; a sustained rate of warnings with no matching returns means the pool is shrinking and will exhaust. Wire the count into the same dashboard as the pool metrics described in Prometheus and Grafana Pool Metrics.
Is it safe to run in production permanently?
Yes for the overwhelming majority of services. The cost is one scheduled task and one stack capture per borrow. Only extremely high-frequency, extremely short borrows make that measurable, and in that case measure before disabling.