Mesrai
Back to blog
// essayTechnical Deep Dive

Connection Pool Exhaustion: The Hidden await Mesrai Found

Real PR catch: `withConn(...)` without await — connections leak under error rate, pool drains. Await + structured error handling fix.

Mesrai TeamAugust 3, 20268 min read

A `withConn` helper that acquires a database connection, runs a function, releases. Pattern looks right. A caller higher up did not `await` the result — the helper's `finally` block never gets a chance to fire on rejected promises until the unawaited promise chain resolves.

The vulnerable diff

tscallers/some-handler.ts
// db/with-conn.ts — the helper looks correct
export async function withConn<T>(fn: (c: PoolClient) => Promise<T>): Promise<T> {
  const conn = await pool.connect();
  try   { return await fn(conn); }
  finally { conn.release(); }
}

// callers/some-handler.ts — but a caller forgot await
async function handler(req, res) {
  // BUG: no await. If fn rejects, the rejection bubbles up but
  // the caller's finally (if any) does not fire — and worse,
  // the WHOLE handler returns before withConn completes.
  withConn(c => doWork(c, req.body)).catch(err => log.error(err));
  res.json({ ok: true });
}

What is wrong

The `withConn` helper is correct in isolation — `try/finally` releases the connection regardless of how `fn` resolves. The bug is at the call site: an unawaited call lets the response return before the connection work finishes, and any rejection that fires after the response has been sent is logged but the surrounding lifecycle (timeouts, request cancellation) does not propagate cleanly. Under error conditions the connection can be released slowly or not at all, depending on the framework's promise lifetime tracking. Symptom in production: pool exhausted, every request hangs on connection acquisition, then times out.

The attack

Reproducer: cause `doWork` to reject in 5% of requests:

textexploit
# Watch pool status during load:
$ watch -n 1 'psql -c "SELECT count(*) FROM pg_stat_activity WHERE usename=$$app$$"'
# Healthy: 8-12 connections.
# Under 5% error rate without await: monotonically grows to pool max,
# then requests stall on pool.connect().

Recovery: restart the app process. Production fix: actually await.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 2 min ago

[mesrai] [code-review] [Performance] [Pool] [high]

`withConn(...)` called without await. The handler returns
immediately. The connection cleanup in withConn's `finally` does
fire eventually, but under error conditions the timing is
non-deterministic and the connection can stay checked out longer
than expected.

Plus: errors that happen after the response is sent are useless
for telling the client what went wrong.

Two correct shapes:

  // Fire-and-forget that actually awaits inside a wrapper:
  ctx.waitUntil(withConn(c => doWork(c, req.body)));

  // Or: synchronous response after the work is done:
  await withConn(c => doWork(c, req.body));
  res.json({ ok: true });

Never an unawaited withConn — the database cleanup contract requires
the caller to await.

The fix

tscallers/some-handler.ts (fixed)
// callers/some-handler.ts — fixed
async function handler(req, res) {
  try {
    await withConn(c => doWork(c, req.body));
    res.json({ ok: true });
  } catch (err) {
    log.error(err);
    res.status(500).json({ error: "internal error" });
  }
}

Await the work. Let the connection helper's `finally` execute synchronously in the request lifecycle. The error path now returns a proper status code to the client and frees the connection before the response is sent. For genuinely fire-and-forget work, use the framework's deferred-work primitive (`ctx.waitUntil`, BullMQ, etc.) — never an unawaited promise from a request handler.

Why human review missed it

Missing await is the most common JS bug class because it looks like working code. The TypeScript signature does not catch it for `Promise<T>` return types — only for explicit `void` returns. Mesrai catches unawaited promises returned from helpers like `withConn` that own resource-lifecycle contracts.

Related rules + further reading

Mesrai rule pack: performance/await-resource-helpers — flags unawaited calls to functions that manage connections/files/locks.

Connection-pool exhaustion is one of the top causes of production outages year after year.

TypeScript's `no-floating-promises` ESLint rule covers the general case.

Takeaway

Resource-lifecycle helpers must be awaited. Unawaited cleanup is leaked cleanup. Mesrai flags every unawaited `withConn`-shape call.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.