Diagnose active, idle, idle-in-transaction, blocked, and long-running sessions with pg_stat_activity, pg_blocking_pids, pg_locks, wait events, and safe cancel-versus-terminate decisions.

pg_stat_activity, Sessions, Transactions, Blocking, and Long-Running Work

Diagnose active, idle, idle-in-transaction, blocked, and long-running sessions with pg_stat_activity, pg_blocking_pids, pg_locks, wait events, and safe cancel-versus-terminate decisions.

Intermediate → Advanced180–240 minutesPostgreSQL observability and incident diagnosisCurrent patched PostgreSQL 18.x; verify current minor at lab timeCore PostgreSQL; pg_stat_statements + auto_explain are PostgreSQL-supplied modulesServiceHub disposable objects: app.ch21_*Observer/admin role recommended; some statistics/signaling require pg_read_all_stats or pg_signal_backend/superuserFree local tooling; OS iostat/vmstat/top equivalents are optional corroborating evidenceLast reviewed: August 2026

Learning outcomes

ServiceHub's API suddenly reports timeouts. One dashboard says “12 long queries,” but that label does not tell us whether those sessions are computing, waiting for a lock, waiting for the client, or sitting idle inside old transactions while holding locks and Multiversion Concurrency Control (MVCC) snapshots. PostgreSQL observability starts by distinguishing current session state from cumulative historical counters.

01

Read pg_stat_activity state, timestamps, transaction age, query ID, and wait-event fields as independent evidence.

02

Reproduce a blocker/waiter incident and identify the blocking PID with pg_blocking_pids before inspecting pg_locks.

03

Explain why idle and idle in transaction are operationally different.

04

Choose pg_cancel_backend versus pg_terminate_backend based on what must be stopped and who may signal the target.

05

Correlate application_name, PID, query_id, and log fields so one incident can be followed across views and logs.

Mental model

pg_stat_activity answers “what is each backend doing now?” A backend is a server process serving a session. state describes the session/transaction command state; wait_event_type/wait_event describe a resource wait. An active backend may still be waiting, and an idle backend may still be inside a transaction.

1. Build one disposable table and name every lab session

sql · ServiceHub blocking lab
DROP TABLE IF EXISTS app.ch21_work_order_lock;SET ROLE servicehub_owner;CREATE TABLE app.ch21_work_order_lock (  work_order_id bigint PRIMARY KEY,  status text NOT NULL,  version integer NOT NULL DEFAULT 1,  note text);INSERT INTO app.ch21_work_order_lock VALUES(21001,'assigned',1,'chapter 21 lock target'),(21002,'queued',1,'independent row');RESET ROLE;

Use three psql terminals: blocker, waiter, and observer. Set a different application_name in each session. Application names make the same sessions recognizable in pg_stat_activity, CSV/JSON logs, and many external monitoring tools.

2. Session A holds a row-changing transaction open

sql · session A — blocker
SET application_name = 'ch21_blocker';SELECT pg_backend_pid() AS blocker_pid;BEGIN;UPDATE app.ch21_work_order_lockSET status = 'in_progress',    version = version + 1WHERE work_order_id = 21001;-- Do not COMMIT yet.

After the UPDATE finishes, psql waits for the next client command. The backend is no longer executing SQL, but the transaction remains open and still owns locks plus its transaction state. In pg_stat_activity it should become idle in transaction, not ordinary idle.

3. Session B waits on the same row

sql · session B — waiter
SET application_name = 'ch21_waiter';SELECT pg_backend_pid() AS waiter_pid;UPDATE app.ch21_work_order_lockSET note = 'waiting update'WHERE work_order_id = 21001;-- This statement waits until session A ends its transaction.

Session B is active because a statement is executing, but it is not necessarily consuming CPU. Its wait-event fields should indicate a lock-related wait while the conflicting transaction remains open.

4. Observe state, age, query identity, and wait separately

sql · observer — current session evidence
SET application_name = 'ch21_observer';SELECT pid,       usename,       application_name,       state,       backend_start,       xact_start,       query_start,       state_change,       now() - xact_start AS transaction_age,       now() - query_start AS query_age,       wait_event_type,       wait_event,       backend_xid,       backend_xmin,       query_id,       left(query, 120) AS query_excerptFROM pg_stat_activityWHERE application_name LIKE 'ch21_%'ORDER BY pid;

xact_start is the right timestamp for an open-transaction age; query_start can instead describe the last statement start even after the session is idle. state_change tells when the current state began. A monitoring rule that uses only “query age” can incorrectly label an idle session as a currently running query.

Cross-user activity visibility is privilege-sensitive. Ordinary roles can inspect their own sessions, while an observability role typically needs pg_read_all_stats (or superuser-equivalent administration) to see complete information about other users' sessions and query text.

5. Ask PostgreSQL who blocks whom

sql · blocking graph
SELECT w.pid AS waiting_pid,       w.application_name AS waiting_app,       w.wait_event_type,       w.wait_event,       b.blocking_pid,       ba.application_name AS blocking_app,       ba.state AS blocker_state,       now() - ba.xact_start AS blocker_xact_ageFROM pg_stat_activity AS wCROSS JOIN LATERAL unnest(pg_blocking_pids(w.pid)) AS b(blocking_pid)LEFT JOIN pg_stat_activity AS ba  ON ba.pid = b.blocking_pidWHERE w.application_name = 'ch21_waiter';

pg_blocking_pids() understands PostgreSQL lock compatibility and wait-queue ordering, including soft blockers ahead in a wait queue. It is safer than writing a casual self-join on pg_locks and guessing lock conflicts.

6. Use pg_locks to explain the lock objects after identifying the blocker

sql · lock-level evidence for the two sessions
WITH lab AS (  SELECT pid  FROM pg_stat_activity  WHERE application_name IN ('ch21_blocker','ch21_waiter'))SELECT l.pid,       a.application_name,       l.locktype,       l.mode,       l.granted,       l.relation::regclass AS relation,       l.page,       l.tuple,       l.transactionid,       l.virtualxidFROM pg_locks AS lJOIN lab USING (pid)LEFT JOIN pg_stat_activity AS a USING (pid)ORDER BY l.pid, l.granted, l.locktype, l.mode;

Row-change waits often surface through transaction-ID locking rather than as a simple “tuple row” in pg_locks. PostgreSQL stores tuple lock information partly in row headers; the lock manager view is not a complete row-lock inventory. Use pg_locks to understand the current lock-manager objects, not as a promise that every locked row appears as one tuple row.

7. Idle in transaction is a risk even without visible blocking

An old idle-in-transaction session can hold locks, keep an MVCC snapshot alive, delay vacuum cleanup, preserve catalog versions, and consume connection capacity. It is therefore a different operational state from a session that is simply idle outside a transaction.

sql · rank open idle transactions
SELECT pid,       usename,       application_name,       state,       xact_start,       now() - xact_start AS xact_age,       backend_xmin,       left(query,120) AS last_queryFROM pg_stat_activityWHERE state IN ('idle in transaction','idle in transaction (aborted)')ORDER BY xact_start NULLS LAST;

Do not automatically terminate the oldest row. First identify ownership, business operation, locks, replication/migration tasks, and whether the session is performing a legitimate long transaction.

8. Cancel stops a current query; terminate ends the session

sql · prefer query cancellation when the query is the problem
SELECT pg_cancel_backend(pid)FROM pg_stat_activityWHERE application_name = 'ch21_waiter';

pg_cancel_backend interrupts the current statement, leaving the session connected. This is appropriate when the statement itself is the unwanted work. It does not end an idle transaction because there is no current statement to cancel.

sql · termination is a larger intervention
SELECT pg_terminate_backend(pid, 5000)FROM pg_stat_activityWHERE application_name = 'ch21_blocker';

pg_terminate_backend disconnects the session; its open transaction is rolled back and locks are released. Signaling another backend is privileged: superusers, suitable role members, or roles with pg_signal_backend may signal eligible non-superuser backends; only superusers can signal superuser backends. The optional timeout waits for termination confirmation.

Wrong approach

“Kill every query older than 60 seconds” confuses legitimate analytics, idle transactions, lock waits, CPU work, maintenance, and client waits. Classify state + wait + blocker + business owner first; choose cancel or terminate only after estimating impact.

9. Correlate with logs by application, PID, and query identifier

conf · useful log correlation prefix
log_line_prefix = '%m [%p] %q%u@%d/%a Q=%Q '

%p is backend PID, %a is application name, and %Q can carry the current query identifier when query IDs are computed. This lets an incident timeline connect activity views, lock waits, pg_stat_statements, and server logs without depending on raw SQL text alone.

10. Resolve and verify

sql · final state after blocker ends
SELECT pid, application_name, state,       wait_event_type, wait_eventFROM pg_stat_activityWHERE application_name LIKE 'ch21_%'ORDER BY application_name;SELECT *FROM app.ch21_work_order_lockWHERE work_order_id = 21001;

After Session A commits, rolls back, or is terminated, Session B either proceeds or has already been canceled. Verify the final business row before declaring the incident resolved. Session-level remediation is not equivalent to application-state correctness.

Check your understanding

  1. Why is state='active' not enough to conclude a backend is using CPU?
  2. Which timestamp best measures how long a transaction has been open?
  3. Why prefer pg_blocking_pids over a hand-written pg_locks self-join for blocker discovery?
  4. Why can pg_cancel_backend fail to solve an idle-in-transaction blocker?
  5. What additional privilege is commonly granted to a non-superuser operations role that must signal eligible backends?
Review the answers

An active backend can be waiting on a lock/I/O/client-related event. xact_start measures transaction age. pg_blocking_pids understands lock conflicts and wait-queue blockers. Cancel targets the current query; an idle transaction has no active query, so terminating the session or asking the owner to end the transaction is required. pg_signal_backend is the predefined signaling capability, excluding superuser backends.

Authoritative references

Statistics and logging fields evolve across PostgreSQL majors. These PostgreSQL 18 primary sources define the mechanisms used in this lesson.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.