Connection Pooling with HikariCP
Reuse database connections with HikariCP, understand the settings that matter, and size the pool for the database rather than for the traffic.
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url); config.setUsername(user); config.setPassword(password);
config.setMaximumPoolSize(10);
HikariDataSource pool = new HikariDataSource(config); // close() on shutdownOpening a database connection is slow by request standards: a TCP handshake, often TLS, authentication, and a new process or thread on the database server. Doing that for every HTTP request adds latency and load. A pool opens connections ahead of time and lends them out: getConnection() borrows one and close() hands it back. HikariCP is the standard pool for Java and the default in Spring Boot.
Setting it up
Create one HikariDataSource at start-up, pass it around as a plain DataSource, and close it on shutdown. A pool per request or per repository defeats the purpose, and can exhaust the database's connection limit within minutes.
The settings that matter
| Setting | Default | What it does |
|---|---|---|
maximumPoolSize | 10 | Most connections the pool will hold, in use or idle. |
minimumIdle | same as maximum | Idle connections kept ready. Leaving it equal gives a fixed-size pool, which HikariCP recommends. |
connectionTimeout | 30000 ms | How long getConnection() waits for a free connection before throwing. |
maxLifetime | 1800000 ms | Retires connections after 30 minutes. Keep it a little below any limit the database or a proxy enforces. |
leakDetectionThreshold | 0 (off) | Logs a warning when a connection is held longer than this. |
Sizing
A bigger pool is not a faster one. The database can only run as many queries at once as it has cores and disks to serve them; past that, extra connections queue inside the database instead of inside the pool, using memory while they wait. HikariCP's pool-sizing guidance starts from cores * 2 + effective spindle count on the database server, and the result is usually smaller than people expect. Remember the multiplier: six replicas with a pool of 20 is 120 connections, and PostgreSQL's default max_connections is 100.
With virtual threads a server can have thousands of requests in flight, but the pool still caps how many talk to the database at once. That cap is useful back-pressure. Set connectionTimeout well below your HTTP timeout so a saturated pool fails quickly with a 503 instead of letting requests pile up.
Leaks
A connection that is borrowed and never closed is lost to the pool. When they are all lost, every getConnection() waits for the full timeout and then throws SQLTransientConnectionException. Use try-with-resources everywhere, and turn on leakDetectionThreshold (for example 20 seconds) in staging: HikariCP then logs the stack trace of the code that borrowed the connection and has not returned it.
Example
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
public final class Pool {
private Pool() {}
public static HikariDataSource create() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(System.getenv("DB_URL")); // jdbc:postgresql://db:5432/shop
config.setUsername(System.getenv("DB_USER"));
config.setPassword(System.getenv("DB_PASSWORD"));
config.setPoolName("orders-db"); // appears in logs and metrics
config.setMaximumPoolSize(10);
config.setConnectionTimeout(5_000); // ms; fail fast when saturated
config.setMaxLifetime(1_500_000); // 25 minutes
config.addDataSourceProperty("ApplicationName", "orders-service");
return new HikariDataSource(config);
}
}
final class App {
static DataSource start() {
HikariDataSource pool = Pool.create();
// Close the pool when the JVM shuts down so connections end cleanly.
Runtime.getRuntime().addShutdownHook(new Thread(pool::close));
return pool; // repositories only see DataSource
}
}When to use it
- Serve hundreds of concurrent API requests through ten database connections without opening a socket per request.
- Return 503 quickly when the pool is exhausted instead of letting requests hang until the load balancer gives up.
- Track down a connection leak in staging from the stack trace HikariCP logs when leak detection is on.
More examples
The Maven dependency
HikariCP is needed at compile time because your start-up code creates HikariConfig; the driver is still runtime only. In a Spring Boot project, leave the versions out and let the Boot BOM pick them.
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>7.0.2</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.13</version>
<scope>runtime</scope>
</dependency>Configuring the pool in Spring Boot
Spring Boot creates a HikariDataSource from these properties when HikariCP is on the class path, which it is with spring-boot-starter-jdbc. The values are passed to HikariCP's setters, so they are plain milliseconds.
spring:
datasource:
url: jdbc:postgresql://db:5432/shop
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
pool-name: orders-db
maximum-pool-size: 10
connection-timeout: 5000 # milliseconds
max-lifetime: 1500000 # milliseconds
leak-detection-threshold: 20000 # milliseconds; useful in stagingReading pool statistics
A steadily rising waiting count means requests are queueing for connections: either queries are slow, connections are leaking, or the pool is too small for the load. Export these numbers to your metrics system rather than guessing.
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
final class PoolStats {
static String describe(HikariDataSource pool) {
HikariPoolMXBean stats = pool.getHikariPoolMXBean();
return "active=" + stats.getActiveConnections()
+ " idle=" + stats.getIdleConnections()
+ " total=" + stats.getTotalConnections()
+ " waiting=" + stats.getThreadsAwaitingConnection();
}
}
Discussion