← Back to the library
BackendApplied · 8 min

Requests hang waiting for a database connection

“Under load, latency jumps to the pool timeout, the database CPU is fine, and logs show connection-acquire timeouts or "too many clients".”

ERROR CODES YOU MAY SEE

A code is a clue. Use its meaning and surrounding evidence to narrow the cause.

53300PostgreSQL SQLSTATE · standard

The server refused a new connection because no connection slots were available (too_many_connections).

Count connections per application and role in pg_stat_activity. Total pool size across every instance, worker, and function must stay below max_connections minus reserved slots.

Official reference for 53300 (opens in new tab)Code definition reviewed
25P03PostgreSQL SQLSTATE · standard

The session was terminated because it sat idle inside an open transaction longer than idle_in_transaction_session_timeout.

Find the code path that opens a transaction and then awaits network calls or user input; that path also holds a pooled connection and its locks.

Official reference for 25P03 (opens in new tab)Code definition reviewed
57014PostgreSQL SQLSTATE · platform

The query was canceled (query_canceled).

Read the accompanying message to identify the cancellation cause. Check timeouts and query traces; cancellation alone does not establish N+1 access.

Official reference for 57014 (opens in new tab)Code definition reviewed

THE PRINCIPLE

Connections are a budget: sum every pool across every process and keep the total below what the database can serve concurrently.

FIRST MOVES

  1. Graph pool waiting/idle/total counts next to pg_stat_activity grouped by state and application_name.
  2. Look for `idle in transaction` sessions: they hold a slot and locks while the app awaits I/O.
  3. Release connections in finally blocks; never hold a transaction open across HTTP calls.
  4. Size pools as instances × pool max ≤ max_connections minus reserved slots; set acquire, statement, and idle-in-transaction timeouts.
  5. For serverless or many short-lived workers, put PgBouncer (transaction mode) or RDS Proxy in front.

TOOLS: THEN → NOW

Raise max_connections when errors appearBudget pools per service and multiplex through a pooler
One connection per serverless invocationPgBouncer, RDS Proxy, or a provider's pooled endpoint

PATTERN SNAPSHOT

const pool = new Pool({ max: 10, connectionTimeoutMillis: 2_000 });

const client = await pool.connect();
try {
  await client.query("BEGIN");
  // Only database work here: no fetch(), no queue publish.
  await client.query("COMMIT");
} catch (err) {
  await client.query("ROLLBACK");
  throw err;
} finally {
  client.release();
}

CLOSE THE AI. EXPLAIN THIS.

Why can a bigger pool make a saturated database slower instead of faster?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed