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.

SyntaxHikariConfig config = new HikariConfig(); config.setJdbcUrl(url); config.setUsername(user); config.setPassword(password); config.setMaximumPoolSize(10); HikariDataSource pool = new HikariDataSource(config); // close() on shutdown

Opening 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

SettingDefaultWhat it does
maximumPoolSize10Most connections the pool will hold, in use or idle.
minimumIdlesame as maximumIdle connections kept ready. Leaving it equal gives a fixed-size pool, which HikariCP recommends.
connectionTimeout30000 msHow long getConnection() waits for a free connection before throwing.
maxLifetime1800000 msRetires connections after 30 minutes. Keep it a little below any limit the database or a proxy enforces.
leakDetectionThreshold0 (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

Example · java
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.

Example · xml
<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.

Example · yaml
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 staging

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

Example · java
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

  • Be the first to comment on this lesson.