The Connection Leak That Took Down Our Production Database
Source: Dev.to
It was 3 AM. PagerDuty woke me up. Our API was returning 500 errors.
The database was fine. CPU was fine. Memory was fine. But every query was timing out.
FATAL: too many connections for role "app_user"
We had exhausted our 100‑connection limit even though traffic was normal. Where were all the connections going? After hours of debugging we found the culprit.
The connection leak hiding in our codebase
// ❌ Bad: missing client.release()
async function getUserOrders(userId) {
const client = await pool.connect();
const orders = await client.query('SELECT * FROM orders WHERE user_id = $1', [
userId,
]);
return orders.rows;
// Where's client.release()? 🤔
}
Every call leaked a connection. With 50 requests/minute we exhausted the pool in 2 minutes.
Common leak scenarios
| Scenario | Result |
|---|---|
Forgot release() entirely | Connection never returned |
Early return before release() | Connection leaked |
| Exception thrown | finally block missing |
| Async error | Unhandled rejection, no cleanup |
Fixing the leak
Always release in a finally block
// ✅ Good: release in finally
async function getUserOrders(userId) {
const client = await pool.connect();
try {
const orders = await client.query(
'SELECT * FROM orders WHERE user_id = $1',
[userId],
);
return orders.rows;
} finally {
client.release(); // Always executes
}
}
Prefer pool.query() for simple queries
// ✅ Best pattern: use pool.query() directly
async function getUserOrders(userId) {
const orders = await pool.query('SELECT * FROM orders WHERE user_id = $1', [
userId,
]);
return orders.rows;
}
Detecting missing releases with ESLint
npm install --save-dev eslint-plugin-pg
// .eslintrc.js
import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];
The rule no-missing-client-release flags any pool.connect() call where client.release() is not guaranteed on all code paths.
src/orders.ts
3:17 error 🔒 CWE-772 | Missing client.release() detected
Fix: Add client.release() in finally block or use pool.query() for simple queries
What the rule checks
- Every
pool.connect()call - Every possible execution path through the function
- Whether
client.release()is called on all paths - Whether it is placed inside a
finallyblock (recommended)
Results after adoption
- 0 connection leaks in 6 months
- No more 3 AM pages for connection exhaustion
- CI now catches issues before they reach staging
Get started
npm install --save-dev eslint-plugin-pg
import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];
Rule docs: no-missing-client-release
Package: eslint-plugin-pg (npm)
Don’t wait for the next 3 AM wake‑up call.