PostgreSQL · beginner

PostgreSQL connection-pool checklist

A practical checklist for engineers diagnosing PostgreSQL failure surfaces where application requests queue waiting for pooled database connections. Walks through observable pool exhaustion symptoms, evidence boundaries between the application pool and the PostgreSQL backend, and verification criteria before changing pool size or query behavior.

The symptoms

  • Application threads or request handlers block on a pool acquire call, with latency climbing while CPU and database CPU remain comparatively low.
  • Pool metrics show active connections at or near the configured maximum, with wait queues growing and zero idle connections.
  • PostgreSQL backend count on the server equals max_connections minus reserved slots, even though application throughput has not increased proportionally.
  • Timeouts such as "connection timed out after" or "pool is full" surface in logs at the same moment as rising p99 latency on dependent services.
  • Intermittent failures concentrated on a small subset of endpoints that issue long-running queries or hold transactions across multiple round trips.

Likely causes

  • Pool maximum size is set below the application's concurrency requirement, so requests serialize behind a small fixed pool.
  • Connections are leaked by code paths that do not return them on exception, so the working pool shrinks over the lifetime of the process.
  • Long-running transactions or slow queries hold connections far longer than expected, reducing effective throughput per connection.
  • PostgreSQL max_connections is reached because multiple application instances multiplied pool sizes that were safe for a single instance.
  • Network or DNS resolution between application and PostgreSQL stalls, causing pool acquire calls to wait beyond their configured timeout.
  • Pool warm-up on startup opens connections faster than PostgreSQL can accept, briefly saturating the server's connection slots.

First ten minutes

  1. 01Confirm the failure surface: which service is returning pool acquisition errors or elevated latency, and whether the database server itself reports saturation.
  2. 02Capture the current pool metrics: maximum size, active count, idle count, and pending waiters. Compare against the configured ceiling to determine whether the pool is at capacity.
  3. 03Capture the current PostgreSQL backend count from pg_stat_activity and compare against the server's max_connections and any superuser_reserved_connections.
  4. 04Identify whether the queueing began after a deploy, configuration change, or traffic change by aligning timestamps across application metrics and database metrics.
  5. 05Decide whether the bottleneck is the application pool, the database connection limit, or query duration before changing any setting.

Evidence to collect

  • Pool-level counters: maximum size, active, idle, pending, acquire wait time histogram, and acquisition timeout configuration.
  • PostgreSQL pg_stat_activity rows grouped by state (active, idle, idle in transaction, idle in transaction aborted, fastpath function call), with application_name, query start time, and wait_event.
  • Server-level configuration values for max_connections, superuser_reserved_connections, and any connection-scaling parameters currently in effect.
  • Application log lines around the queueing window showing pool exhaustion messages, timeouts, and the originating endpoint or query.
  • Recent change log including pool size edits, query changes, deploys, and traffic patterns for the same window.

Where to look

  • Application pool boundary: the library's statistics endpoint or metrics export where active, idle, and wait counters are exposed.
  • PostgreSQL boundary: the pg_stat_activity and pg_settings catalog views, documented in the PostgreSQL monitoring statistics documentation linked below.
  • Network boundary: connection-establishment latency between the application tier and the PostgreSQL listener, including any intermediary proxy or load balancer.
  • Query boundary: the slowest queries observed during the queueing window, including their mean and p99 duration and whether they hold transactions.
  • Configuration boundary: the application's pool configuration file and the PostgreSQL postgresql.conf or equivalent parameter source.

Diagnostic steps

  1. 01Read pg_stat_activity and group rows by state and wait_event; a backlog of "active" rows with non-trivial query_start age points to slow queries, while many "idle in transaction" rows point to application-side transaction handling.
  2. 02Compare the number of application-named backends in pg_stat_activity to the configured pool maximum; if they match and idle count is zero, the application pool is the ceiling.
  3. 03Compare the total backend count to PostgreSQL max_connections; if it is at the ceiling, the server itself is the bottleneck regardless of pool size.
  4. 04Inspect acquire-wait time distribution over the queueing window; a sharp rise indicates queueing, while uniformly high waits point to slow connection establishment.
  5. 05Cross-reference pool exhaustion timestamps with deploy markers and traffic shifts to isolate whether the change was configuration, code, or load.
  6. 06Inspect wait_event values on active backends: ClientRead, DataFileRead, and lock waits each indicate different upstream causes and rule out different fixes.

Common mistakes

  • Raising pool maximum size without verifying whether PostgreSQL max_connections has headroom for the additional connections across all application instances.
  • Restarting the application to "free" connections when the real cause is a connection leak in a code path that does not return connections on error.
  • Increasing statement or query timeouts to mask queueing, which hides the symptom without addressing the pool ceiling or query duration.
  • Assuming slow queries are unrelated to pool exhaustion; long transactions reduce effective pool throughput even when the pool itself is not full.
  • Treating intermittent failures as network blips without checking whether they correlate with pool saturation windows.

Safe fixes

  • Conditional on evidence that the application pool is the ceiling: reduce per-request work holding a connection, or introduce a per-request acquisition timeout so failures fail fast rather than queue indefinitely.
  • Conditional on evidence of leaked connections: identify the request paths that do not release on exception and add a closing block or equivalent resource cleanup, verified by stable idle plus active counts over time.
  • Conditional on evidence of long transactions: shorten transaction scope so connections return to the pool promptly, verified by a drop in idle-in-transaction backends in pg_stat_activity.
  • Conditional on evidence that PostgreSQL max_connections is the ceiling: introduce or right-size a connection pooler between the application and PostgreSQL, so application concurrency no longer maps one-to-one to backends.
  • Conditional on evidence of slow connection establishment: investigate the network path between the application and the database listener and resolve any added latency before tuning pool sizes.

Prove the fix

  1. 01Active plus idle connections remain below the pool maximum during a representative traffic window, with non-zero idle count indicating headroom.
  2. 02Acquire-wait time p99 returns to its pre-incident baseline for at least one full traffic cycle, and pending waiter count stays at zero during peak.
  3. 03PostgreSQL backend count remains below max_connections minus superuser_reserved_connections with the same workload.
  4. 04No new idle-in-transaction backends accumulate during the verification window, confirming transactions are scoped correctly.
  5. 05Application p99 latency on dependent endpoints returns to its pre-incident baseline, with no timeout errors in logs for the verification window.

Prevention and next steps

  • Define an explicit SLO for pool utilization and acquire-wait time, and alert before the pool reaches saturation rather than after requests begin queueing.
  • Track pool acquisition timeout configuration alongside pool maximum size in version control, and review both when scaling the number of application instances.
  • Periodically sample pg_stat_activity to confirm idle-in-transaction backends are not accumulating, and review the slowest queries against their expected duration.
  • Document the relationship between application concurrency, per-instance pool size, and PostgreSQL max_connections so capacity changes are made with headroom in mind.

Safe commands and checks

SELECT state, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;
SELECT application_name, state, count(*), max(now() - query_start) AS oldest_query FROM pg_stat_activity GROUP BY 1,2 ORDER BY 3 DESC;
SELECT name, setting FROM pg_settings WHERE name IN ('max_connections','superuser_reserved_connections');
SELECT pid, usename, application_name, state, wait_event, wait_event_type, now() - query_start AS query_age FROM pg_stat_activity WHERE state <> 'idle' ORDER BY query_age DESC NULLS LAST;
SELECT count(*) FILTER (WHERE state = 'active') AS active, count(*) FILTER (WHERE state = 'idle') AS idle, count(*) FILTER (WHERE state LIKE 'idle in transaction%') AS idle_in_tx FROM pg_stat_activity;