Files
hibernate-demo/docs/19-hikaricp-connection-pooling.md
T

9.6 KiB

19 — HikariCP connection pooling

← Previous: 18 — Ehcache 3 L2 cache configuration | Back to README →

Backs the rewrite of ankurm.com post 4886 (HikariCP connection pooling).

The claim that needs no repo at all to check, and is still worth checking

"Spring Boot uses HikariCP by default" is one of those facts everyone repeats and almost no one verifies against their own project. This repo's own application.yml names no connection pool at all -- no spring.datasource.type, no spring.datasource.hikari.* block -- and the DataSource bean Spring Boot 4.1.1 hands out anyway really is a HikariDataSource:

RESULT[hikari-spring-default]: dataSource class=com.zaxxer.hikari.HikariDataSource | pool name=HikariPool-1 | maximumPoolSize=10 | minimumIdle=10 | connectionTimeout=30000ms | idleTimeout=600000ms -- these are HikariCP's own built-in defaults (maximumPoolSize=10, minimumIdle defaults to maximumPoolSize), not anything this project set.

(full transcript), source: SpringAutoConfiguredHikariTest

Confirmed against HikariConfig.class itself (disassembled from HikariCP-7.0.2.jar): maxPoolSize defaults to 10, minIdle defaults to -1 (meaning "unset"), and validate() resolves an unset minIdle up to whatever maxPoolSize ends up being -- which is exactly the minimumIdle=10 this test observes with nothing configured.

Bootstrapping HikariCP with zero Spring involved

The original article's own example is plain Hibernate, no Spring Boot. Reproducing that faithfully needed one more dependency this repo didn't already have: org.hibernate.orm:hibernate-hikaricp, the integration jar that teaches a raw StandardServiceRegistry how to talk to HikariCP at all -- test-scoped, since the Spring-managed chapters never need it.

StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
        .applySetting("hibernate.connection.provider_class", "hikari")
        .applySetting("hibernate.hikari.maximumPoolSize", "7")
        .applySetting("hibernate.hikari.poolName", "hibernate-demo-ch19-pool")
        .applySetting("hibernate.hikari.connectionTimeout", "5000")
        // ... driver_class, url, username as usual
        .build();

HikariRawBootstrapTest.java

RESULT[hikari-raw-bootstrap]: ConnectionProvider class=org.hibernate.hikaricp.internal.HikariCPConnectionProvider | isUnwrappableAs(HikariDataSource)=true
RESULT[hikari-raw-bootstrap-config]: poolName=hibernate-demo-ch19-pool | maximumPoolSize=7 | connectionTimeout=5000ms -- every value traces back to a hibernate.hikari.* setting passed into StandardServiceRegistryBuilder, with zero Spring involved.

(full transcript)

Two things confirmed by disassembling hibernate-hikaricp-7.4.5.Final.jar rather than assumed from the property name: hibernate.connection.provider_class accepts the short aliases hikari or hikaricp, not only the fully-qualified class name (both are registered in StrategyRegistrationProviderImpl); and every hibernate.hikari.*-prefixed setting has that prefix stripped and is handed to new HikariConfig(properties) as-is -- HikariConfigurationUtil declares the prefix itself as the public constant CONFIG_PREFIX = "hibernate.hikari.". That means any HikariConfig setter is reachable this way, not just the handful mentioned here.

Trap: case matters. hibernate.hikari.maximumPoolSize maps to HikariConfig#setMaximumPoolSize because HikariCP's own property loader does exact, case-sensitive bean-property matching -- hibernate.hikari.maximumpoolsize (all lowercase) silently does nothing rather than failing loudly.

Failure mode 1: pool exhaustion is a named exception, not a hang

Lead with what actually breaks. Every connection in the pool checked out, and one more request comes in:

config.setMaximumPoolSize(1);
config.setConnectionTimeout(1000);
Connection held = dataSource.getConnection();
// a second dataSource.getConnection() from here...

HikariPoolExhaustionTest.java

RESULT[hikari-pool-exhaustion]: maximumPoolSize=1, connectionTimeout=1000ms | second getConnection() waited=1004ms before throwing java.sql.SQLTransientConnectionException: exhaustion-pool - Connection is not available, request timed out after 1002ms (total=1, active=1, idle=0, waiting=0)

(full transcript)

This is worth knowing verbatim because it's exactly what you'll grep application logs for: a SQLTransientConnectionException (a real, distinct exception type -- catchable separately from other SQL errors), naming the pool by name, stating the timeout, and reporting the pool's live total/active/idle/waiting counts at the moment it gave up. SQLTransientConnectionException being a java.sql.SQLTransientException also means a retry framework that specifically retries transient SQL errors will treat this one as retryable by default -- worth confirming that's actually what you want under sustained load, rather than assuming it.

  • Going deeper: raising maximumPoolSize is the obvious fix, but it isn't free -- each connection is a real OS thread and socket on both ends; HikariCP's own sizing guidance (rel="nofollow") argues for pool sizes much smaller than most defaults assume, based on connections = ((core_count * 2) + effective_spindle_count).

Failure mode 2: leak detection has a floor the article never mentioned

"Set leakDetectionThreshold" is the standard advice for catching code that checks out a connection and forgets to close it. What isn't standard advice: HikariCP enforces a hard floor on that setting, and violating it fails silently rather than loudly.

config.setLeakDetectionThreshold(500); // under the floor
HikariDataSource ds = new HikariDataSource(config); // validate() runs right here

HikariLeakDetectionTest.java

RESULT[hikari-leak-threshold-floor]: requested leakDetectionThreshold=500ms | actual leakDetectionThreshold after construction=0ms | logged warnings=1 | message=HikariPool-1 - leakDetectionThreshold is less than 2000ms or more than maxLifetime, disabling it. -- HikariCP does not clamp 500ms up to 2000ms, it disables leak detection entirely and logs a WARN naming the reason.

(full transcript)

Confirmed by disassembling HikariConfig.class: any value under 2000ms (and any value above maxLifetime, when maxLifetime is set) is reset straight to 0 -- disabled -- inside validate(), which runs synchronously the moment new HikariDataSource(config) is called. It does not round up to 2000ms and it does not throw; it just quietly turns the feature off and logs one WARN through HikariConfig's own logger. A value at or above the floor behaves as documented:

RESULT[hikari-leak-detection]: leakDetectionThreshold=2000ms | logger=com.zaxxer.hikari.pool.ProxyLeakTask | level=WARN | message=Connection leak detection triggered for conn1: url=jdbc:h2:mem:hikarileak user=SA on thread main, stack trace follows

(same transcript)

Trap: if you copy a "fast" leak-detection setting from a unit test into production config -- 200ms, 500ms, anything under two seconds -- HikariCP accepts it without error and simply never checks for leaks at all. The only sign is a single WARN logged once, at startup, that's easy to miss in a noisy boot log.
  • Going deeper: the leaked-connection warning includes a captured stack trace of the original checkout site (visible in the full transcript) -- that's what makes this setting worth turning on in staging even when it costs a little overhead, since it points straight at the offending code path rather than just the symptom.

Production checklist

  • Don't trust "HikariCP is the default" from memory for a project you haven't checked -- confirm the actual DataSource bean type, the way SpringAutoConfiguredHikariTest does here.
  • Size the pool from real concurrency needs, not a round number -- HikariCP's own guidance argues for pools smaller than most defaults assume.
  • Set connectionTimeout deliberately: it's what turns pool exhaustion from "the caller hangs" into "the caller gets a typed, catchable exception in a bounded time."
  • Set leakDetectionThreshold to at least 2000ms if you set it at all -- anything lower is silently a no-op, not a more sensitive check.
  • In a non-Spring bootstrap, remember hibernate.hikari.* property names are case-sensitive bean property names, not free-form config keys.

← Previous: 18 — Ehcache 3 L2 cache configuration | Back to README →