Skip to main content

HikariCP with Hibernate 7: What the Default Pool Actually Does

Spring Boot uses HikariCP by default — but confirming what that actually means takes a running app, not memory. This post measures the real default pool size, a pool-exhaustion failure that’s a named exception rather than a hang, and an undocumented 2-second floor on leakDetectionThreshold that fails silently below it.

Every connection your application makes to the database costs a handshake, sometimes a TLS negotiation, and an authentication round trip. HikariCP is the connection pool Spring Boot reaches for without being asked, and this post checks what it actually guarantees — the real default pool size, what happens the moment every connection is checked out, and a leak-detection setting that fails silently rather than loudly — against a running Spring Boot app and a raw, non-Spring Hibernate bootstrap.

Versions used in this post. Hibernate 7.4.5.Final, Spring Boot 4.1.1, JDK 25, HikariCP 7.0.2 (via Spring Boot’s own dependency management), hibernate-hikaricp 7.4.5.Final for the raw, non-Spring bootstrap. Every code and output block below links to the exact file in the companion repository it came from.

The connection you don’t want to pay for twice

Opening a JDBC connection means a TCP handshake, a TLS negotiation if the database requires it, and an authentication round trip — all before the first query runs. Pay that cost on every request and it dominates your latency budget under load. A connection pool exists to pay it once per connection and then hand the already-open connection out and back thousands of times. HikariCP is the pool Spring Boot reaches for by default, and this post checks what it actually does — not what a decade of “HikariCP is fast” blog posts assume — against a running application and a raw, non-Spring bootstrap.

Paying the connection cost once, not per request No pool Request 1: handshake + TLS + auth Request 2: handshake + TLS + auth Request 3: handshake + TLS + auth Connection cost paid every time HikariCP pool Startup: open N connections once Request 1, 2, 3… borrow + return No handshake per request Connection cost paid N times total

Confirming “Spring Boot uses HikariCP by default” against your own project, not memory

Everyone repeats this fact; almost no one checks it against the project in front of them. This repository’s application.yml names no pool at all — no spring.datasource.type, no spring.datasource.hikari.* block — and the DataSource 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

From the transcript, source: SpringAutoConfiguredHikariTest.java.

These aren’t anything this project set — they’re HikariCP’s own built-in defaults, confirmed by disassembling HikariConfig.class from HikariCP-7.0.2.jar: maxPoolSize defaults to 10, and minIdle defaults to -1 (meaning “unset”), which validate() resolves up to whatever maxPoolSize ends up being — exactly the minimumIdle=10 observed above with nothing configured.

Worth doing on your own project, not just this one: log dataSource.getClass() once at startup. It costs one line and it’s the difference between knowing your pool configuration and assuming it.
  • Going deeper: the full chapter 19 doc lists every default HikariCP field this run observed.

Bootstrapping HikariCP with zero Spring involved

The classic, pre-Spring-Boot way to wire Hibernate to HikariCP directly is still worth knowing — some services never touch Spring at all. It needs one extra, test-scoped dependency this repo didn’t otherwise carry: org.hibernate.orm:hibernate-hikaricp, the integration jar that teaches a raw StandardServiceRegistry how to talk to HikariCP.

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();

From 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

From the transcript.

Two things confirmed by disassembling hibernate-hikaricp-7.4.5.Final.jar, not assumed from the property name: hibernate.connection.provider_class accepts the short aliases hikari or hikaricp, registered in StrategyRegistrationProviderImpl, not only the fully-qualified class name; and every hibernate.hikari.*-prefixed setting has that prefix stripped and is handed straight to new HikariConfig(properties) — the prefix itself is declared as the public constant HikariConfigurationUtil.CONFIG_PREFIX. That means any HikariConfig setter is reachable this way, not just the handful shown 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 — you’ll get the default pool size and no error telling you why.

For the intermediate reader: this “strip a prefix, reflectively set bean properties” mechanism is worth contrasting with a different config-loading trap that looks similar but isn’t — hibernate.javax.cache.uri‘s classpath-prefix requirement passes a raw string straight to a resource loader instead.

  • Going deeper: the full chapter 19 doc lists every disassembled class involved in this bootstrap path.

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

Lead with what actually breaks in production: every connection checked out, and one more request comes in.

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

From 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 1000ms (total=1, active=1, idle=0, waiting=0)

From the transcript.

Pool exhaustion, maximumPoolSize=1 Thread A holds the one connection Thread B waits up to connectionTimeout Timeout reached SQLTransientConnectionException total=1, active=1, idle=0, waiting=0 — reported at the moment it gives up The exception names the pool, states the timeout, and reports live counts — grep-able, not a mystery hang.

SQLTransientConnectionException is a real, distinct exception type — catchable separately from other SQL errors — and it’s a java.sql.SQLTransientException, which also means a retry framework configured to retry transient SQL errors will treat this one as retryable by default. Worth confirming that’s what you actually want under sustained load rather than assuming it.

  • Going deeper: raising maximumPoolSize is the obvious fix but not free — each connection is a real OS thread and socket on both ends. HikariCP’s own sizing guidance argues for pools much smaller than most defaults assume, based on connections = ((core_count * 2) + effective_spindle_count).

Failure mode 2: leak detection has an undocumented floor

“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

From 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.

From the transcript. Confirmed by disassembling HikariConfig.class: any value under 2000ms (or above maxLifetime, when set) is reset straight to 0 — disabled — inside validate(), which runs synchronously the moment new HikariDataSource(config) is called. It doesn’t round up to 2000ms and it doesn’t throw; it quietly turns the feature off and logs one WARN.

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

From the same transcript.

Trap: a “fast” setting copied from a unit test into production is a no-op, not a stricter check. 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, easy to miss in a noisy boot log.
  • Going deeper: the leak 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 at a small cost, since it points straight at the offending code path.

Should you tune any of this yourself?

For most Spring Boot applications, the honest answer is: size the pool, set a connection timeout deliberately, and leave the rest at HikariCP’s defaults — they’re well-reasoned, not arbitrary. Only reach for the raw, non-Spring bootstrap in this post if you’re actually outside Spring; inside Spring Boot, spring.datasource.hikari.* properties reach the same HikariConfig without needing hibernate-hikaricp at all.

  • Don’t trust “HikariCP is the default” from memory for a project you haven’t checked — confirm the actual DataSource bean type.
  • Size the pool from real concurrency needs, not a round number.
  • Set connectionTimeout deliberately: it’s what turns exhaustion from “the caller hangs” into “the caller gets a typed, bounded-time exception.”
  • Set leakDetectionThreshold to at least 2000ms if you set it at all — anything lower is silently a no-op.
  • In a non-Spring bootstrap, remember hibernate.hikari.* property names are case-sensitive bean-property names, not free-form config keys.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.