Tuning HikariCP maxLifetime and idleTimeout
This guide is part of HikariCP Configuration Deep Dive. It covers the two properties that decide when a pooled connection is thrown away rather than reused, and the single most common production error they exist to prevent: Connection is closed or An I/O error occurred while sending to the backend arriving on a connection the pool believed was healthy.
Those errors almost never originate in your application. Something in the network path — a cloud load balancer, a firewall, a PgBouncer instance, the database’s own idle_session_timeout — quietly dropped a TCP connection that the pool still had on its idle list. The pool then handed that dead socket to a request. maxLifetime is the property that prevents it, by guaranteeing the pool retires connections before anything else in the path does.
The two properties do different jobs
They are frequently confused because both retire connections. The distinction is what triggers retirement:
maxLifetimeis an age cap. Every connection, idle or not, is retired once it has existed for this long. It applies regardless of how busy the pool is.idleTimeoutis a shrink control. A connection that has sat unused for this long is retired only if the pool currently holds more connections thanminimumIdle.
The second half of that sentence is the part people miss. If minimumIdle equals maximumPoolSize — which is HikariCP’s default, because minimumIdle defaults to the same value as maximumPoolSize — then the pool is a fixed-size pool and idleTimeout does nothing at all. Setting idleTimeout on a fixed-size pool is a no-op, and a great deal of tuning effort has been wasted on that fact.
Setting maxLifetime against the network path
The rule is mechanical. Enumerate every component between your JVM and the database that can close an idle connection, take the shortest of their timeouts, and set maxLifetime comfortably below it. HikariCP’s own recommendation is at least 30 seconds of margin, and its documented default is 1,800,000 ms (30 minutes).
| Component in the path | Property that closes idle connections | Typical value |
|---|---|---|
| AWS Network Load Balancer | idle timeout, not configurable below 350 s on some paths | 350 s |
| AWS RDS Proxy | IdleClientTimeout |
1800 s default |
| PgBouncer | server_idle_timeout |
600 s default |
| PostgreSQL 14+ | idle_session_timeout |
0 (disabled) unless set |
| PostgreSQL | tcp_keepalives_idle |
often 7200 s |
| Azure SQL / gateway | idle connection reset | ~30 min |
| Corporate firewall / NAT | flow table eviction | 300–900 s, often undocumented |
The classic misconfiguration is leaving maxLifetime at 30 minutes behind an NLB whose idle timeout is 350 seconds. Every connection that sits unused for six minutes is silently severed at the load balancer; HikariCP has no idea and hands it out; the request fails with an I/O error. Setting maxLifetime to 1,740,000 ms (29 minutes) does not help here — the binding constraint is 350 seconds, not 30 minutes.
spring:
datasource:
hikari:
pool-name: orders-pool
maximum-pool-size: 20
minimum-idle: 20 # fixed size: idle-timeout is inert
max-lifetime: 280000 # 280 s, comfortably under a 350 s NLB timeout
idle-timeout: 0 # explicit no-op, documents the intent
keepalive-time: 120000 # probe idle connections every 2 minutes
connection-timeout: 30000
validation-timeout: 5000
keepaliveTime, added in HikariCP 4.0, is the complement to maxLifetime. It periodically runs a lightweight probe on idle connections, which both validates them and generates traffic that resets idle timers in intermediate devices. Set it to roughly a third to a half of the shortest external idle timeout. It must be less than maxLifetime, and HikariCP will reject values that are not.
Two special cases are worth naming:
maxLifetime = 0disables the age cap entirely. Only correct when nothing in the path ever closes an idle connection, which in cloud environments is essentially never true.- HikariCP applies up to 2.5% random jitter to each connection’s effective lifetime. This is deliberate: without it, a pool created at startup would retire every connection within the same second, producing a synchronised reconnect storm against the database.
Setting idleTimeout deliberately
idleTimeout matters when a database’s connection budget is shared across services and you want a bursty service to give connections back between peaks. That requires deciding to run a variable-size pool:
spring:
datasource:
hikari:
maximum-pool-size: 30
minimum-idle: 8 # floor the pool shrinks to
idle-timeout: 300000 # 5 min unused, and above the floor -> retire
max-lifetime: 280000
Now the pool holds 8 connections at rest and grows to 30 under load, releasing the surplus five minutes after the peak passes. HikariCP requires idleTimeout to be at least 10 seconds, and warns if it is within a second of maxLifetime, in which case it ignores it.
The trade-off is real and should be made consciously. A variable-size pool returns connections to the shared budget, but every growth event pays connection-establishment cost — TCP handshake, TLS negotiation, authentication, and on PostgreSQL a backend process fork — inside a request’s latency budget. HikariCP’s own guidance is to run a fixed-size pool for latency-sensitive services precisely because of this. Prefer variable sizing when the constraint is the database’s total connection count; prefer fixed sizing when the constraint is tail latency. The arithmetic for both cases is in Connection Pool Sizing Formulas.
Common failure patterns and remediation
| Symptom | Underlying cause | Remediation |
|---|---|---|
Intermittent Connection is closed after quiet periods |
maxLifetime longer than a load balancer or firewall idle timer |
Lower maxLifetime below the shortest external timer; add keepaliveTime |
idleTimeout set but pool never shrinks |
minimumIdle still equals maximumPoolSize |
Lower minimumIdle explicitly |
| Reconnect storms every 30 minutes | Pool created at once, all connections aging out together | Rely on HikariCP’s built-in lifetime jitter; do not disable it by pinning lifetimes externally |
| Latency spikes after idle periods | Variable-size pool re-establishing connections in the request path | Raise minimumIdle, or move to a fixed-size pool |
| Errors only against a read replica | Replica endpoint sits behind a different proxy with a shorter timer | Configure the replica data source separately rather than sharing settings |
maxLifetime ignored at startup |
Value below the 30 s floor, silently reset to the default | Use at least 30000; check startup logs for the reset warning |
| Connections retired constantly under load | maxLifetime set very low as a workaround for stale sockets |
Fix the underlying idle timer or add keepaliveTime instead of churning connections |
Verifying the settings work
Configuration that is never verified is a guess. Two checks close the loop.
First, confirm the pool is actually retiring on schedule. HikariCP logs pool statistics at DEBUG, and the connection count should stay flat while maxLifetime cycles connections underneath:
logging:
level:
com.zaxxer.hikari.pool.HikariPool: DEBUG
Second, prove the stale-connection failure is gone by reproducing the conditions that caused it. Leave the service idle for longer than the shortest external timeout, then issue a single request. Before the fix that request fails; after it, the connection was already retired and replaced, so it succeeds. On PostgreSQL you can watch the server side of the same cycle through pg_stat_activity, using the technique in PostgreSQL Server-Side Connection Diagnostics — the backend_start column shows connections being replaced on the cadence you configured.
Operational boundary
These two properties control when connections are discarded. They do nothing about connections that are held too long by application code — that is what the leak detection threshold is for — and nothing about how many connections exist, which is maximumPoolSize. Reaching for a shorter maxLifetime to fix an exhaustion problem is a category error: retiring connections faster does not create more of them.
Frequently Asked Questions
What is a safe default for maxLifetime?
Does maxLifetime interrupt a query in progress?
Why do I see connections older than maxLifetime?
Should I use keepaliveTime instead of maxLifetime?
keepaliveTime keeps idle connections alive and validated between uses; maxLifetime guarantees an upper bound on age so a connection can never outlive the infrastructure’s tolerance. keepaliveTime must be shorter than maxLifetime.Does idleTimeout apply to connections below minimumIdle?
minimumIdle, remaining idle connections are kept indefinitely regardless of how long they have been unused. Only maxLifetime retires them.How do these interact with a connection proxy?
server_idle_timeout governs the proxy-to-database leg while your maxLifetime governs the application-to-proxy leg; both need to be set. See PgBouncer Transaction vs Statement Pooling for the surrounding configuration.Can I set these differently per data source?
Related
- HikariCP Configuration Deep Dive — the parent guide and the full parameter reference.
- Configuring the HikariCP Leak Detection Threshold — finding code that holds connections too long.
- Connection Pool Sizing Formulas — deciding
maximumPoolSizeandminimumIdle. - Connection Acquisition Timeout Strategies — the timeout side of the same configuration.
- AWS RDS Proxy Connection Pooling — a common source of a shorter external idle timer.