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.
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.maximumPoolSizemaps toHikariConfig#setMaximumPoolSizebecause 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.
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
maximumPoolSizeis 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 onconnections = ((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
DataSourcebean type. - Size the pool from real concurrency needs, not a round number.
- Set
connectionTimeoutdeliberately: it’s what turns exhaustion from “the caller hangs” into “the caller gets a typed, bounded-time exception.” - Set
leakDetectionThresholdto 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
- 📘 Chapter 19 — HikariCP connection pooling, the full companion chapter behind this post
- 📘 Hibernate 7 + Ehcache 3 second-level cache — the other half of the performance stack
- 📘 Batch processing with Hibernate 7 — high-volume inserts that depend on a well-configured pool
- 🔗 HikariCP official configuration guide
- 🔗 Official Hibernate 7 User Guide — HikariCP Integration
No Comments yet!