sre-skills · Guide

PostgreSQL: find the query that's blocking everything

Requests are hanging, connections are piling up, and the database CPU is idle. That pattern almost always means lock contention. Here's how to find the root blocker in under a minute and deal with it safely. The queries are tested on PostgreSQL 17 and work on 9.6 and later.

0. Protect your own session

SET statement_timeout = '10s';
SET lock_timeout = '1s';

This stops your diagnostic queries from joining the queue themselves.

1. Who is blocked, and by whom?

SELECT blocked.pid AS blocked_pid, now() - blocked.query_start AS blocked_for,
       left(blocked.query, 80) AS blocked_query,
       blocking.pid AS blocking_pid, blocking.state AS blocking_state,
       now() - blocking.xact_start AS blocking_xact_age,
       left(blocking.query, 80) AS blocking_query
FROM pg_stat_activity blocked
JOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS b(pid) ON true
JOIN pg_stat_activity blocking ON blocking.pid = b.pid
ORDER BY blocked_for DESC;

pg_blocking_pids() does the hard work of matching lock modes, so you don't need the old self-join on pg_locks. Run it as a role with pg_monitor. Otherwise other users' queries show as <insufficient privilege>.

2. Find the root blocker

Blocking forms chains: A blocks B, and B blocks C, D and E. Killing B does nothing. You want the pid that blocks others but isn't blocked itself:

SELECT pid, state, now() - xact_start AS xact_age, left(query, 100) AS query
FROM pg_stat_activity
WHERE pid IN (SELECT unnest(pg_blocking_pids(pid)) FROM pg_stat_activity)
  AND cardinality(pg_blocking_pids(pid)) = 0;

3. Recognise the pattern

Root blocker looks likeWhat's happening
state = idle in transaction, old xact_ageAn app (or someone's psql session) opened a transaction, took locks, and never committed. Common causes are an exception path that skips rollback, or a slow HTTP call made inside a transaction.
ALTER TABLE … waiting, with everything behind itA migration needs ACCESS EXCLUSIVE, but it's queued behind a long-running SELECT. Every new query then queues behind the migration. This is the classic "the migration took the site down" incident.
A long UPDATE/DELETEA batch job touching many rows that other transactions also need.

4. Act, carefully

SELECT pg_cancel_backend(<pid>);     -- cancel the current query (gentle)
SELECT pg_terminate_backend(<pid>);  -- kill the session and roll back its transaction

Try cancel first. It does nothing to an idle in transaction session, because there's no running query to cancel, so those need terminate. Terminating rolls back the session's work, so make sure you know what the transaction was doing. A half-done data migration rolled back is fine. A payment that was mid-way through needs checking.

5. Prevent it

Let Claude Code do this for you. The postgres-incident skill in sre-skills Pro runs this diagnosis plus connection exhaustion, replication lag and slots, slow queries, vacuum and wraparound checks. It's read-only, and it asks before cancelling anything.

The free sre-skills pack covers incident triage, Kubernetes, CI, Terraform, logs and postmortems.