During peak traffic, a Spring Boot service may occasionally report an error similar to this:
Could not get JDBC ConnectionHikariPool-1 - Connection is not available, request timed out after 30000msRequests then slow down or time out across multiple endpoints. Restarting the service appears to fix the problem, but only until the pool becomes exhausted again.
This error means that a thread waited for a database connection longer than HikariCP’s connectionTimeout. It does not identify the underlying cause. The pool might be leaking connections, queries might be holding them too long, or application concurrency might simply exceed the database capacity.
Increasing maximumPoolSize can postpone the next timeout, but it can also overload the database. The right fix begins with measurements.
First, Clarify the Runtime Assumptions
Spring Boot 3 uses HikariCP by default when the library is available, but it does not strictly require HikariCP. Another supported pool implementation can be selected through dependencies and configuration.
Spring Boot 3 also requires Java 17 or later. Java 17 improves the language and runtime, but it does not automatically close JDBC resources. Virtual threads are a Java 21 feature, not a Java 17 feature. These distinctions matter because upgrading the JDK does not correct a connection leak or increase database capacity.
What Pool Exhaustion Actually Means
A HikariCP pool has a fixed upper bound. When every connection is in use, new callers wait for one to be returned. If none becomes available before connectionTimeout, the caller receives an exception.
Pool exhaustion usually comes from one or more of these conditions:
- connections are acquired but never closed;
- transactions remain open during network calls or expensive computation;
- slow or blocked SQL holds connections for too long;
- request concurrency is higher than the pool can absorb;
- the database is unavailable or cannot create replacement connections;
- pool sizing ignores the number of application instances and other database clients.
The same symptom can therefore represent a code defect, a query problem, a capacity problem, or an outage.
Cause 1: Leaked JDBC Resources
Calling DataSource.getConnection() borrows a connection from the pool. Calling close() on that pooled connection returns it to the pool. If an exception or early return skips close(), the pool gradually loses usable connections.
Unsafe manual cleanup
public List<Student> findStudents() throws SQLException { Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement( "select id, name from t_student" ); ResultSet resultSet = statement.executeQuery();
List<Student> students = new ArrayList<>(); while (resultSet.next()) { students.add(mapStudent(resultSet)); }
resultSet.close(); statement.close(); connection.close(); return students;}Any exception before the final three calls can leak resources.
Use try-with-resources
public List<Student> findStudents() throws SQLException { String sql = "select id, name from t_student";
try (Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement(sql); ResultSet resultSet = statement.executeQuery()) {
List<Student> students = new ArrayList<>(); while (resultSet.next()) { students.add(mapStudent(resultSet)); } return students; }}Resources are closed in reverse order even when execution fails. Prefer framework-managed access through Spring JDBC, Spring Data, JPA, or MyBatis-Spring when practical; use try-with-resources whenever application code owns the JDBC resources.
HikariCP can also log connections that remain checked out longer than a diagnostic threshold:
spring: datasource: hikari: leak-detection-threshold: 20000This setting is a diagnostic aid, not a leak detector with perfect accuracy. Legitimately slow operations can trigger it, so choose a threshold above normal query latency and investigate the recorded acquisition stack trace. Avoid setting it aggressively and forgetting it in production.
Cause 2: Transactions That Include Remote I/O
Spring normally binds a JDBC connection to the current transaction when database access first requires one. Once acquired, that connection remains associated with the transaction until completion.
This method performs an unpredictable network call after a database update:
@Transactional(rollbackFor = Exception.class)public void updateStudent(Student student) { studentMapper.updateInfo(student); remoteClient.notifyThirdParty(student.id());}While the HTTP call waits, the transaction and its connection remain open. Under load, a small number of slow remote calls can occupy the entire pool. They also extend lock duration.
Keep the database transaction focused on database work:
@Servicepublic class StudentService { private final StudentTransactionService transactions; private final RemoteClient remoteClient;
public void updateStudent(Student student) { remoteClient.validate(student); transactions.updateStudent(student); }}
@Servicepublic class StudentTransactionService { private final StudentMapper studentMapper;
@Transactional(rollbackFor = Exception.class) public void updateStudent(Student student) { studentMapper.updateInfo(student); }}The transactional method is placed on a separate Spring bean so the call passes through the transaction proxy. Merely moving it to another method on the same class would create a self-invocation and usually bypass @Transactional advice.
Do not reorder operations blindly. If the remote system must be notified only after a successful commit, consider an after-commit event or the transactional outbox pattern. Distributed consistency is a separate design decision from shortening a local database transaction.
Cause 3: Slow or Blocked SQL
A connection in use is not necessarily leaked. It may be executing a slow query, waiting for a lock, or processing an unexpectedly large result set.
For MySQL, start with the following tools:
- inspect
SHOW FULL PROCESSLISTorperformance_schemafor long-running and blocked statements; - examine execution plans with
EXPLAINorEXPLAIN ANALYZEwhere appropriate; - check indexes, row estimates, scanned rows, lock waits, and result-set size;
- configure query or transaction timeouts appropriate to the operation;
- review database CPU, storage latency, memory pressure, and connection limits.
Do not assume that every long query needs a larger pool. More concurrent slow queries often increase database contention and make total throughput worse.
Cause 4: Concurrency Exceeds Database Capacity
A database connection pool is also a concurrency limit. If 500 request threads can reach the database but the pool contains 20 connections, at most 20 of those operations can use a connection at once. The rest must wait.
Virtual threads in Java 21 reduce the cost of blocked Java threads, but they do not make JDBC connections or the database unlimited. In fact, allowing much more request concurrency without an explicit downstream limit can make pool contention more visible.
Apply backpressure at the application boundary when necessary. Limit concurrent database work, bound queues, set request deadlines, and make overload fail predictably rather than allowing an unbounded backlog.
Size the Pool as a System, Not an Instance
There is no universal pool-size formula. A useful budget starts with the database’s safe connection capacity:
connections available to this service = database connection budget - administration headroom - connections reserved for other workloads
maximumPoolSize per instance <= connections available to this service / peak instance countThen validate the result with load tests and production measurements. Consider query latency, transaction duration, database CPU and I/O, expected concurrency, autoscaling limits, failover behavior, and connections used by migrations or background jobs.
A conservative starting configuration might look like this, but the numbers are examples rather than recommendations for every system:
spring: datasource: hikari: maximum-pool-size: 12 minimum-idle: 4 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000Keep maxLifetime shorter than any database, proxy, firewall, or load-balancer connection lifetime so HikariCP can retire connections before infrastructure does. Add jitter or follow infrastructure-specific guidance where applicable.
Observe the Pool Before Changing It
With Spring Boot Actuator and Micrometer enabled, monitor HikariCP metrics such as:
hikaricp.connections.active: connections currently in use;hikaricp.connections.idle: idle connections;hikaricp.connections.pending: callers waiting for a connection;hikaricp.connections.timeout: timed-out acquisition attempts;hikaricp.connections.usage: how long connections remain checked out.
For direct Actuator inspection, expose only the endpoints your operational environment requires:
management: endpoints: web: exposure: include: health,metricsSecure Actuator endpoints before exposing them outside a trusted management network. Build alerts around sustained utilization, pending callers, acquisition time, and timeouts rather than alerting only after requests fail.
A Practical Troubleshooting Sequence
When the timeout appears, preserve evidence before restarting if operationally safe:
- Record active, idle, pending, and maximum pool connections.
- Capture thread dumps and find threads waiting in HikariCP or executing database work.
- Inspect database sessions for slow statements, lock waits, and connection churn.
- Correlate the incident with request latency, traffic, deployments, scheduled jobs, and scaling events.
- Enable leak detection temporarily if the evidence suggests connections are not being returned.
- Review manual JDBC code and transaction boundaries.
- Load-test the suspected fix with realistic query latency and the maximum instance count.
The evidence usually separates the main failure modes:
| Signal | Likely direction |
|---|---|
| Active at maximum, pending rising, slow database sessions | Slow SQL, lock contention, or overloaded database |
| Active at maximum, leak warnings with stable acquisition stacks | Unclosed resources or unexpectedly long connection use |
| Pool cannot replenish after database errors | Database availability, networking, credentials, or connection creation |
| Short usage time but frequent pending callers | Pool too small for measured demand, bursty traffic, or missing backpressure |
| Exhaustion follows remote-service latency | Network I/O inside transaction boundaries |
Production Checklist
- Close every manually acquired
Connection,Statement, andResultSetwith try-with-resources. - Keep HTTP, RPC, file I/O, and heavy computation outside database transactions.
- Verify that
@Transactionalcalls cross a Spring proxy and are not self-invocations. - Monitor active, idle, pending, usage, acquisition, and timeout metrics.
- Investigate slow SQL and lock waits before increasing pool size.
- Budget database connections across the maximum number of application instances.
- Set application, transaction, query, and request timeouts coherently.
- Treat virtual threads as cheaper concurrency, not additional database capacity.
- Re-run load and failure tests after changing the JDK, framework, driver, database, or infrastructure.
Final Takeaway
Connection is not available is the final symptom of a constrained resource, not a diagnosis. A larger pool is correct only when measurements show that the database has spare capacity and connections are being used efficiently.
Start by measuring waiters and connection usage. Then inspect resource ownership, transaction duration, SQL performance, database health, and total cross-instance concurrency. Fixing the limiting layer prevents the next incident; restarting the service only resets the clock.