Part 1: Connection Pool Leaks — What They Are and Why They Matter
Series Navigation - Part 1: What They Are and Why They Matter (current) - Part 2: How Spring Helps You Avoid Them - Part 3: Built-in Detection in Major Connection Pools - Part 4: Building a Custom Connection Monitor - Part 5: Root Cause Analysis with Heap Dumps
1. How Connection Pools Work
Establishing a database connection is expensive. It involves a TCP handshake, database authentication, and session initialization — typically taking several to tens of milliseconds. Creating and destroying a connection for every database operation would be a serious performance bottleneck under high concurrency.
Connection pools solve this problem: a set of connections is pre-created and held in a pool. When business code needs a connection, it borrows one from the pool; when finished, it returns the connection rather than closing it.
+-------------------------------------------+
| Connection Pool |
| |
| +------+ +------+ +------+ +------+ |
| | conn | | conn | | conn | | conn | |
| +------+ +------+ +------+ +------+ |
| idle connections |
| |
| +------+ +------+ |
| | conn | | conn | <-- in use |
| +------+ +------+ |
+-------------------------------------------+
getConnection() ---> borrow from idle
conn.close() ---> return to idle (not actually closed)
The core contract of a connection pool:
- getConnection() — borrow a connection from the pool
- conn.close() — return the connection to the pool (not actually close it)
2. What Is a Connection Leak?
A connection leak occurs when a borrowed connection is never returned to the pool.
Normal flow:
borrow → use → return (conn.close()) → connection back in pool ✅
Leaked flow:
borrow → use → close() forgotten or skipped on exception
→ connection never returns to pool ❌
A single leak has limited impact, but leaks accumulate. Over time, the number of available connections in the pool decreases until it is completely exhausted.
3. Symptoms of a Connection Leak
| Symptom | Description |
|---|---|
Request timeout or Unable to acquire connection error |
The pool is exhausted; new requests cannot get a connection |
| Active connection count keeps growing and never drops | Leaked connections are counted as "active", consuming pool capacity |
| Database-side connection count keeps growing | Leaked connections still hold resources on the database side |
| Service recovers after restart, then degrades again after some time | Classic pattern of a resource leak |
| Connection pool wait queue length continuously increases | Requests are waiting for an available connection |
4. Common Causes of Connection Leaks
Cause 1: Forgetting to call close()
public User findUser(Long id) throws SQLException {
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?");
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
User user = mapRow(rs);
// ❌ conn.close() is never called
return user;
}
Cause 2: Connection not closed on the exception path
public User findUser(Long id) throws SQLException {
Connection conn = dataSource.getConnection();
try {
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?");
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
User user = mapRow(rs); // if this throws an exception...
conn.close(); // ❌ this line is never reached
return user;
} catch (Exception e) {
// conn is not closed on the exception path → leak
throw e;
}
}
Fix: use try-with-resources or a finally block:
// ✅ Option 1: try-with-resources (recommended)
public User findUser(Long id) throws SQLException {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?")) {
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
return mapRow(rs);
// conn is automatically closed whether or not an exception occurs
}
}
// ✅ Option 2: finally block
public User findUser(Long id) throws SQLException {
Connection conn = null;
try {
conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?");
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
return mapRow(rs);
} finally {
if (conn != null) {
conn.close(); // always executed
}
}
}
Cause 3: Long-running transactions holding connections
public void processLargeData() throws SQLException {
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
for (int i = 0; i < 1_000_000; i++) {
// processing millions of rows, running for minutes
// the connection is held the entire time
// it will eventually be released, but behaves like a leak in the meantime
process(conn, i);
}
conn.commit();
conn.close();
}
5. How Leaks Lead to Pool Exhaustion
Timeline:
t=0 pool has 20 connections
t=10 3 requests leak connections → 17 available
t=20 5 more requests leak connections → 12 available
...
t=N 0 connections available
new requests time out waiting → exception thrown
service unavailable ❌
Connection leaks are a progressive problem. It can take hours or even days from the first leak to a full outage, which is why the root cause is often difficult to identify at the moment symptoms appear.
6. Summary
| Concept | Description |
|---|---|
| Connection leak | A borrowed connection is never returned to the pool |
| Root cause | close() is not called — either missed in the normal path or skipped on an exception path |
| Final impact | Pool exhaustion, service unavailability |
| Progressive nature | There is a time window between the first leak and a full outage; a restart temporarily restores service |
This series is based on Druid 1.2.x, HikariCP 5.x (Java 11+; use HikariCP 3.x for Java 8), Tomcat JDBC 10.x, and DBCP2 2.9.x. All code examples are for reference only. Adjust them to your specific environment before using in production.