Skip to main content
In this lesson we diagnose and fix a common Postgres problem: your application exhausts available database connections. This typically appears during traffic spikes and can bring critical flows (like checkout) to a halt. Learn how to investigate live connections, apply immediate mitigations, fix the application pattern, and adopt architectural improvements to prevent recurrence. Scenario
  • Ten application servers.
  • One Postgres database.
  • Each server keeps a connection pool of 10 connections under normal load and can grow to 20 under heavy load.
  • Postgres is configured with max_connections = 200.
During a flash sale, traffic triples and Postgres logs this error:
Your checkout is down. What’s the correct response? A common but hasty reaction is to raise the Postgres connection limit (for example, max_connections = 500) so the app servers can open more connections. That can buy time but has serious drawbacks: each Postgres connection maps to an OS process and consumes memory even when idle. Raising max_connections can cause the database to spend more time and memory managing processes than executing queries.
Before changing max_connections, inspect what connections actually exist — often the real cause is different. In this scenario, the autoscaler added more servers during the spike. Four extra servers were provisioned, explaining the additional connections: 4 servers × 10 normal connections = 40 extra connections, bringing the usual total to ~140. Under peak load each server’s pool can grow to 20 connections, which may exhaust the 200-connection limit.
A dark infographic showing an autoscaler and many servers opening lots of connections converging on a Postgres database. Text highlights a 156/200 connection count and notes that under load each pool went from 10 to 20 connections.
Investigate live connections with pg_stat_activity Postgres exposes pg_stat_activity which lists every live connection and what it’s doing. Run this as a superuser or a role with sufficient access:
If many sessions show idle in transaction, that is a red flag: the application opened a transaction, then waited (for an external call, user input, etc.) while still holding the DB connection. Example problematic output:
Root cause in this scenario
  • The checkout code begins a DB transaction, performs writes, then calls an external payment provider before committing.
  • The external call can be slow (tens of seconds), so the transaction holds the connection while doing no DB work.
  • Many concurrent checkouts holding transactions lead to connection exhaustion.
Fix this in stages Stage 1 — Immediate mitigation (bring the service back up)
  • Terminate stuck sessions so new checkouts can proceed. Postgres will roll back open transactions safely.
  • Configure an automatic timeout to prevent transactions remaining idle inside a transaction for too long. Use idle_in_transaction_session_timeout to kill sessions that stay idle in a transaction beyond a sensible threshold:
Setting idle_in_transaction_session_timeout prevents long-running idle in transaction sessions from accumulating. Choose a value that accommodates legitimate long transactions for your workload, but protects the server from resource exhaustion.
Stage 2 — Fix the application
  • Never hold a DB connection while waiting on a slow external operation. Commit and release the connection before making external network calls (e.g., payment provider). If you need to persist the external result, obtain a short-lived connection after the external call to write the result.
Example (Python / psycopg2-like pseudocode):
Stage 3 — Architectural improvement: put a PgBouncer in front of Postgres
  • PgBouncer is a lightweight connection pooler/proxy that sits between your application and Postgres.
  • Applications can open many cheap client connections to PgBouncer while PgBouncer keeps a small, fixed pool of real Postgres connections.
  • In transaction pooling mode, PgBouncer assigns a real Postgres connection only for the duration of a transaction; once the transaction completes the connection returns to the pool. This dramatically reduces the number of concurrent Postgres processes.
A dark-themed diagram titled "Stage 3: A Bouncer at the Door" showing multiple client/server icons funneling many cheap connections into a central "PG BOUNCER" box, which then forwards through a small fixed pool of real connections to a Postgres database.
Be careful when using PgBouncer in transaction pooling mode: session-level features (temporary tables, session-local GUC settings, prepared statements) do not persist across pooled transactions. Some ORMs or application patterns require session pooling instead. Choose the pooling mode and application changes that match your workload.
Summary checklist Links and references Follow this flow: observe before changing limits, mitigate quickly to restore service, fix the application pattern that holds connections, and adopt a pooler when appropriate. Only after you understand the memory and CPU implications should you consider increasing max_connections.

Watch Video