The error message tells you the pool ran out of connections. It does not tell you why. Before you change a setting, you need to know whether the pool is leaking connections, holding them too long, deadlocking on itself, failing to create new ones, or just too small.
HikariCP throws SQLTransientConnectionException: Connection is not available, request timed out after 30000ms when a thread waits longer than connectionTimeout for a free connection. Every connection was busy and the pool could not add one in time.
Five different causes produce the same message:
- Connection leak: code borrows a connection and never closes it.
- Long hold: slow queries, long transactions, or HTTP calls made inside a transaction.
- Pool deadlock: a thread holding one connection waits for a second one.
- Creation failure: the pool is below its maximum but cannot open new connections.
- Undersized pool: hold times are healthy, but throughput × hold time exceeds the pool size.
Raising maximumPoolSize or connectionTimeout is the most common suggestion, and it is only the right fix for the last cause. This guide shows how to collect the evidence that tells the five causes apart. That evidence includes pool metrics, leak detection traces, thread dumps, and database session state, plus a map of the code paths that acquire and release connections.
What does “Connection is not available, request timed out” mean?
A HikariCP pool keeps a fixed maximum number of physical database connections (maximumPoolSize, default 10). When application code calls getConnection(), HikariCP hands over an idle connection. If none is idle and the pool is already at its maximum, the calling thread waits. If the wait exceeds connectionTimeout (default 30,000 ms), the call fails.
java.sql.SQLTransientConnectionException: HikariPool-1 -
Connection is not available, request timed out after 30000ms (total=10, active=10, idle=0, waiting=4)
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java)
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java)
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java)
Recent HikariCP versions append the pool statistics to the message. Those four numbers are the first piece of evidence, and most people skip them.
| Field | Meaning | What it hints at |
|---|---|---|
total | Physical connections currently open | If total < maximumPoolSize while threads wait, the pool is failing to create connections |
active | Connections borrowed by application code | Pinned at the maximum means every connection is out; the question is where |
idle | Connections sitting in the pool, ready | Should be zero at the moment of a timeout; if not, look for a very short connectionTimeout |
waiting | Threads blocked in getConnection() | How much demand is queued behind the busy connections |
If HikariCP failed to create a connection recently, it attaches that failure as the exception’s cause. A Caused by: ... Connection refused, an authentication error, or too many connections from the database changes the diagnosis completely. Read the whole stack trace before reading the pool configuration.
Five causes, one error message
Developer questions about this error on Stack Overflow keep circling the same confusion. The pool is exhausted. Is it a leak, a long running query, or a pool that is simply too small? The honest answer is that the error cannot tell you. The evidence around it can.
1. Connection creation failure
The pool is not full, yet threads time out. HikariCP is trying to open new physical connections and failing or taking too long. Common reasons are the database hitting max_connections (often because several services or replicas share it), network or DNS problems, TLS handshakes that stall, or credentials that were rotated. The stats show total below the maximum, and the exception usually carries the underlying cause.
2. Connection leak
A code path borrows a connection and never returns it. Each leak permanently removes one connection from the pool, so the pool degrades step by step until it is empty. The signature is active climbing over hours or days and not coming back down when traffic falls. Restarting the service “fixes” it until the leak catches up again.
3. Long hold: slow queries, long transactions, and remote calls
Nothing is leaked, but each borrower keeps its connection for much longer than expected. A query that used to take 20 ms now takes 2 seconds because of a missing index, lock contention, or a larger table. A transaction wraps an HTTP call, a message publish, or file processing. The connection is returned eventually, so active drops when load drops. Under load it is pinned.
4. Pool deadlock from nested acquisition
A thread that already holds a connection asks the pool for a second one. Common culprits are Propagation.REQUIRES_NEW, a second DataSource.getConnection() inside a transaction, or a callback that opens its own session. With a pool of 10 and 10 concurrent requests that each hold one connection and wait for another, no thread can finish. They all wait until connectionTimeout.
5. Undersized pool
Every borrower behaves correctly and returns its connection quickly, but demand simply exceeds supply. This is the only cause where increasing maximumPoolSize is the fix, and it can only be confirmed after ruling out the other four.
Collect the evidence first
Each cause leaves different fingerprints. Gather these five sources before changing anything, ideally during or right after an incident.
Evidence 1: Pool metrics over time
A single snapshot cannot distinguish a leak from a traffic spike. You need active, idle, pending, and connection usage time as a time series. If you use Micrometer (Spring Boot does this automatically with Actuator), HikariCP publishes:
hikaricp.connections.active,.idle,.pending,.maxhikaricp.connections.usage: how long connections are held between borrow and return. This is the most useful metric here.hikaricp.connections.acquire: how long threads wait to get onehikaricp.connections.timeout: count of timeouts
Without a metrics registry, turn on DEBUG logging for com.zaxxer.hikari. The housekeeper thread logs pool stats periodically (every 30 seconds by default), which is enough to see the trend:
HikariPool-1 - Before cleanup stats (total=10, active=4, idle=6, waiting=0)
HikariPool-1 - Before cleanup stats (total=10, active=7, idle=3, waiting=0)
HikariPool-1 - Before cleanup stats (total=10, active=10, idle=0, waiting=2)
HikariPool-1 - Before cleanup stats (total=10, active=10, idle=0, waiting=9)
Evidence 2: Leak detection stack traces
HikariCP can tell you who borrowed a connection that has been out too long. Leak detection is off by default. Enable it with a threshold above your longest legitimate hold time (the minimum is 2,000 ms):
# Warn when a connection is held longer than 20 s, with the borrower's stack trace
spring.datasource.hikari.leak-detection-threshold=20000
# Periodic pool stats in the logs
logging.level.com.zaxxer.hikari=DEBUG
# Expose HikariPoolMXBean over JMX (active, idle, threads awaiting connection)
spring.datasource.hikari.register-mbeans=true
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setMaximumPoolSize(10);
config.setLeakDetectionThreshold(20_000); // ms; 0 = disabled (default)
config.setRegisterMbeans(true);
When the threshold is crossed, HikariCP logs the stack trace captured at the moment the connection was borrowed. That tells you exactly which code path took it:
Connection leak detection triggered for org.postgresql.jdbc.PgConnection@6b1d4f
on thread http-nio-8080-exec-7, stack trace follows
java.lang.Exception: Apparent connection leak detected
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java)
at com.example.report.ReportDao.export(ReportDao.java:42)
at com.example.report.ReportService.nightlyExport(ReportService.java:88)
Now watch for what happens next. If the same connection is later returned, HikariCP logs a follow-up:
Previously reported leaked connection org.postgresql.jdbc.PgConnection@6b1d4f
on thread http-nio-8080-exec-7 was returned to the pool (unleaked)
Leak detection measures only how long a connection has been out. A warning followed by “unleaked” means the connection came back: that is a long hold, not a leak. A warning with no follow-up, repeating from the same stack trace, is a real leak.
Evidence 3: Thread dumps
A thread dump taken while threads are waiting separates the two populations you care about: threads waiting for a connection, and threads holding one.
# Take two or three dumps ~10 s apart during the incident
jcmd <pid> Thread.print > threads-1.txt
# How many threads are waiting on the pool?
grep -c "HikariPool.getConnection" threads-1.txt
Waiting threads are parked inside the pool:
"http-nio-8080-exec-12" TIMED_WAITING (parking)
at java.util.concurrent.locks.LockSupport.parkNanos
at java.util.concurrent.SynchronousQueue.poll
at com.zaxxer.hikari.util.ConcurrentBag.borrow
at com.zaxxer.hikari.pool.HikariPool.getConnection
at org.springframework.orm.jpa.JpaTransactionManager.doBegin
The holders are more interesting. Look at what threads inside your data access code are doing:
- Blocked in a socket read from the JDBC driver: waiting on the database. Suspect slow queries or locks.
- Inside an HTTP client, message producer, or
Thread.sleepwhile also inside a transaction: a long hold caused by application code. - In
HikariPool.getConnectionwhile already inside a transactional method: nested acquisition. If most waiters look like this, it is a pool deadlock. - No thread appears to own the missing connections: they were leaked by code that has already moved on.
Evidence 4: The database’s view of the sessions
The database sees every one of the pool’s connections and knows what each is doing. In PostgreSQL:
SELECT pid, state, wait_event_type, wait_event,
now() - xact_start AS xact_age,
now() - query_start AS query_age,
left(query, 80) AS last_query
FROM pg_stat_activity
WHERE datname = current_database()
AND backend_type = 'client backend'
ORDER BY xact_start NULLS LAST;
| What you see | What it suggests |
|---|---|
active with a large query_age | A slow query. Check the plan, indexes, and data growth. |
active with wait_event_type = Lock | Lock contention. Another transaction is blocking these sessions. |
idle in transaction with a large xact_age | The application opened a transaction and is doing something else while holding it, such as a remote call or slow processing, or it never committed. |
idle sessions while the pool reports active=max | Connections are borrowed but unused, which is typical of a leak with autocommit on. |
Fewer sessions than maximumPoolSize and connection errors in the log | Creation failure. Check max_connections across every client of this database. |
For MySQL, use SHOW FULL PROCESSLIST and information_schema.innodb_trx for the same picture. Setting the JDBC ApplicationName property makes the pool’s sessions easy to filter.
Evidence 5: The code paths themselves
The first four sources tell you what kind of problem you have. Only the code tells you where it is and how many other places share the same pattern. That gets its own section below.
Leak vs long running query vs undersized pool: how to tell them apart
This is the question most developers get stuck on. The pool looks the same in all three cases at the moment of the timeout. The difference shows up over time, and in where the connection time is spent.
| Signal | Connection leak | Long hold | Pool deadlock | Undersized pool |
|---|---|---|---|---|
active after traffic drops | Stays high, ratchets up | Falls back | Falls after timeouts fire | Falls back |
| Connection usage time | Unbounded for leaked ones | High (p95/p99) | Up to connectionTimeout | Normal |
| Leak detection | Warnings, no “unleaked” | Warnings, then “unleaked” | Possible, then “unleaked” | Quiet |
| Thread dump | No owner for missing connections | Holders in socket read or HTTP call | Holders waiting in getConnection | Holders doing short, normal work |
| Database sessions | idle but borrowed | active or idle in transaction | idle in transaction | Short active queries |
| Correlates with | Uptime; a restart “fixes” it | Specific endpoints, data size, a slow dependency | Concurrency near pool size | Throughput |
The sizing arithmetic
The number of connections a service needs on average follows Little’s Law: throughput × average hold time. That is why a hold time problem and a capacity problem look identical from the outside.
The HikariCP project’s own pool sizing guidance argues for smaller pools than most teams expect. The database can only execute so many queries in parallel, and extra connections add contention rather than throughput. It also gives the minimum size that avoids pool deadlock when each thread needs several connections: Tn × (Cm − 1) + 1, where Tn is the maximum number of threads and Cm the maximum connections a single thread holds at once.
Map the code paths that acquire and release connections
Runtime evidence narrows the cause. It rarely hands you the complete list of code that has the problem. A leak detection trace shows the one path that tripped the threshold today, not the three other methods written the same way. To fix the class of problem rather than the instance, inventory the code paths that touch the pool:
- Acquisition points. Every
DataSource.getConnection(),@Transactionalboundary,TransactionTemplate,EntityManagerorSessionopened manually, and repository method that returns aStream<T>or cursor. - What happens while it is held. SQL only, or also HTTP calls, message publishing, file I/O, retries with backoff,
sleep, or waiting on other threads? - Release on every path. Is
close()guaranteed through try-with-resources or a framework template, including exception and early return paths? - Nested acquisition. Can a thread holding a connection request another through
REQUIRES_NEW, a second data source call, or an event listener that opens its own transaction? - Blast radius. Which endpoints, jobs, and consumers share this pool, and what fails for them when it starves?
Here are the two patterns behind most real incidents, and what the fix looks like.
Leak: close() skipped on the exception path
public List<Row> export() throws SQLException {
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(EXPORT_SQL);
ResultSet rs = ps.executeQuery();
List<Row> rows = toRows(rs); // throws on a bad row: conn is never closed
conn.close();
return rows;
}
public List<Row> export() throws SQLException {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(EXPORT_SQL);
ResultSet rs = ps.executeQuery()) {
return toRows(rs); // closed even if toRows throws
}
}
The same pattern appears in less obvious forms. Examples include a Spring Data repository method returning Stream<T> that the caller never closes, DataSourceUtils.getConnection() without a matching releaseConnection(), and a connection passed to another thread or stored in a field.
Long hold: a remote call inside a transaction
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order); // connection bound at transaction start
paymentClient.charge(order); // 3 s HTTP call: connection held, database idle
order.markPaid();
}
public void placeOrder(Order order) {
Long id = tx.execute(s -> orderRepository.save(order.pending()).getId());
PaymentResult result = paymentClient.charge(order); // no connection held
tx.executeWithoutResult(s -> orderRepository.markPaid(id, result));
}
Splitting the transaction changes the consistency model. The order can now exist in a pending state if the payment call fails, so it needs a status and a retry or reconciliation path. That trade-off is exactly why this should be a reviewed design change, not a pool setting.
spring.jpa.open-in-view defaults to true, and Spring Boot logs a warning about it at startup. It keeps the persistence context open for the whole web request, so lazy loading during serialization or view rendering can extend how long a request depends on a connection. If your hold times are long and the thread dumps point at the web layer, set it to false and fetch what the view needs inside the service.
Fixes, matched to the cause
| Cause | Fix | What not to do |
|---|---|---|
| Connection leak | Find every acquisition path and guarantee release with try-with-resources or framework templates. Close returned streams and cursors. | Raise the pool size. A leak drains any size, just more slowly. |
| Slow query | Fix the query: plan, index, batching, pagination. Consider a statement timeout so one query cannot hold a connection forever. | Raise connectionTimeout. Users wait longer for the same failure. |
| Remote call or slow work inside a transaction | Move I/O outside the transaction, keep transactions short, and add timeouts to the remote client. | Add connections to match the slowest dependency. |
| Pool deadlock | Remove nested acquisition, or size the pool with Tn × (Cm − 1) + 1. Bound worker concurrency below pool capacity. | Assume it is load. It will happen at moderate concurrency too. |
| Creation failure | Fix the chained cause: max_connections budget across all instances, network, credentials, TLS. | Raise maximumPoolSize. That asks the database for even more connections. |
| Undersized pool | Increase maximumPoolSize based on throughput × hold time plus headroom, within what the database can serve. | Size by instinct. Multiply by the number of instances and check the database limit. |
A bigger pool makes a leak take longer to find and a slow dependency take longer to notice.
Evidence to require before suggesting a fix
Pool timeouts get “fixed” by configuration changes that ship quickly, pass review easily, and hide the real problem until the next traffic peak. Whether the fix comes from a teammate, a Stack Overflow answer, or an AI coding assistant, ask for the evidence that matches it:
- Connection usage p99 is normal
activefalls when traffic falls- No leak warnings without “unleaked”
- Database has spare capacity and
max_connectionsheadroom across all instances
- Leak trace with no “unleaked” follow-up
activeratchets up with uptime- The acquisition path is identified in code
- The path that skips
close()is shown
pg_stat_activityor equivalent shows longactivequeries or lock waits- Thread dumps show holders in driver socket reads
- The slow statement is named, with its plan
This is the same discipline we apply to any change that affects shared production resources. The diff alone is not the evidence. The evidence is how the changed code acquires, holds, and releases what it shares, and what else depends on it. For the broader practice, see How to Assess the Blast Radius of a Code Change and Pre Merge Reliability Analysis.
How Tomosu helps
Tomosu analyzes the standing codebase and each change for this kind of production reliability risk. For connection pool exhaustion, the useful output is not another “increase your pool size” suggestion. It is the map from the previous section, with evidence attached:
- The code paths that acquire and release connections: transactional boundaries, manual
getConnection()calls, returned streams, and whether each path releases on every branch. - Risky holds: remote calls, retries, and slow work inside transactions, and nested acquisition that can deadlock the pool.
- Blast radius: which endpoints and jobs share the pool, so a finding in a nightly export is weighed differently from one on the checkout path.
- The evidence needed before a fix: what to check in metrics, leak traces, thread dumps, and the database to confirm the cause before a configuration change is approved.
These signals roll up into the Production Reliability Index, so a pull request that adds an HTTP call inside a @Transactional method is flagged before merge, not after the next peak.
Scan your repository with Tomosu →
Key takeaways
- “Connection is not available, request timed out” means a thread waited longer than
connectionTimeout. It is a symptom, not a cause. - Read the pool stats and the chained cause in the exception first.
totalbelow the maximum points to creation failure. - Leak detection reports apparent leaks. A later “unleaked” message means a long hold, not a leak.
- Connection usage time, not query time alone, separates long holds from an undersized pool.
- Map every code path that acquires and releases connections. The leak trace shows one instance; the map shows the pattern.
- Increase
maximumPoolSizeonly after the evidence rules out leaks, long holds, deadlock, and creation failure.
Frequently asked questions
What does “Connection is not available, request timed out” mean in HikariCP?
It means a thread called getConnection() and HikariCP could not hand it a connection within connectionTimeout (30,000 ms by default). Every connection in the pool was in use and no new one could be created in time. HikariCP throws java.sql.SQLTransientConnectionException. The error is a symptom of pool starvation, not a diagnosis of why the pool was starved.
Is a HikariCP request timeout always a connection leak?
No. The same timeout is produced by a true leak, by slow queries or long transactions that hold connections too long, by remote calls made while a transaction is open, by pool deadlock from nested connection acquisition, by failures creating new connections, and by a pool that is too small for the workload. Each has different evidence and a different fix.
How do I enable HikariCP connection leak detection?
Set leakDetectionThreshold in milliseconds, for example config.setLeakDetectionThreshold(20000) or spring.datasource.hikari.leak-detection-threshold=20000 in Spring Boot. It is disabled by default (0) and the minimum accepted value is 2000 ms. When a connection is held longer than the threshold, HikariCP logs a warning with the stack trace of the code that borrowed it.
Why does HikariCP report a leak and later say the connection was unleaked?
Leak detection only measures how long a connection has been out of the pool. If the connection comes back after the warning, HikariCP logs that the previously reported leaked connection was returned to the pool. That pattern points to a long hold, such as a slow query or a remote call inside a transaction, rather than a true leak.
Should I increase maximumPoolSize to fix the timeout?
Only when the evidence shows the pool is undersized: connection hold times are normal, active returns to low levels when load drops, there are no leak reports, and the database has spare capacity. If connections are leaked or held too long, a larger pool delays the timeout and moves more load onto the database without fixing the cause.
Can @Transactional cause HikariCP pool exhaustion?
Yes. A connection is typically held for the full duration of a transaction. If a @Transactional method makes an HTTP call, waits on a queue, or does slow processing, the connection is held for that time too. Propagation.REQUIRES_NEW inside an existing transaction needs a second connection per thread, which can deadlock a pool under concurrency.
What leakDetectionThreshold value should I use?
Set it above the longest legitimate time your application holds a connection, based on measured connection usage time rather than a guess. A value that is too low floods the logs with long holds that are not leaks. A value that is too high delays the evidence you need during an incident.
A pool timeout is the last step of a longer story: a code path that acquired a connection and did not give it back in time. Tomosu maps those paths before they reach production. Assess your repository →