How to Monitor Slow Running Queries in PostgreSQL
Monitor Slow Running Queries in PostgreSQL with pg_stat_activity
An application hangs, the connection pool fills up, and somewhere a statement has been running for minutes. pg_stat_activity shows it for every backend on the server: how long the current query has been going, which role and database it belongs to, and the wait event it is parked on. The query below filters that view down to active client queries older than 30 seconds, longest first.
Purpose and Overview
A slow-query log records a statement only after it finishes. That is the wrong tool when an application is hanging at this moment and the statement that causes it has not completed. pg_stat_activity shows the live side: one row per server process, with the text of the statement it is executing and the time that statement began.
The view reports a snapshot, not history. A query that finished a second ago is gone, and a query that is still running appears with its start time, so the age of a statement is simply the current time minus query_start. This page covers that live view. For the historical side, the per-statement totals kept by pg_stat_statements, see the pg_stat_statements timing query.
The script returns the process ID, the role, the database, the application name, the state, the wait event, the runtime, and the query text. The process ID is the column that matters most during an incident, because it is the handle that pg_cancel_backend() and pg_terminate_backend() take.
Sample Code
1SELECT
2 pid,
3 now() - query_start AS runtime,
4 usename,
5 datname,
6 application_name,
7 state,
8 wait_event_type,
9 wait_event,
10 left(query, 200) AS query
11FROM pg_stat_activity
12WHERE state = 'active'
13 AND backend_type = 'client backend'
14 AND pid <> pg_backend_pid()
15 AND now() - query_start > interval '30 seconds'
16ORDER BY runtime DESC;
A second variant targets transactions that are open but not running anything, which hold locks and block vacuum without ever showing up as a slow query:
1SELECT
2 pid,
3 now() - xact_start AS transaction_age,
4 now() - state_change AS time_in_state,
5 usename,
6 datname,
7 state,
8 left(query, 200) AS last_query
9FROM pg_stat_activity
10WHERE state IN ('idle in transaction', 'idle in transaction (aborted)')
11ORDER BY transaction_age DESC;
Notes: The first query needs PostgreSQL 10 or later because of backend_type; on 9.6 delete that condition, and on 9.2 through 9.5 also remove wait_event_type and wait_event (the waiting boolean is the older equivalent). Rows for other users' sessions show mostly null columns unless the caller is a superuser or holds the pg_read_all_stats role. Change the 30 seconds literal to match the service's latency budget.
Code Breakdown
The first query is a filter over one view, and each condition removes a different kind of noise.
Runtime from query_start
query_start is the time the currently active query began; when the state is not active, it holds the start of the last query instead. Subtracting it from now() gives the age of the statement. The expression is repeated in WHERE because a column alias cannot be used in a WHERE clause, although ORDER BY accepts the alias.
Filtering on state
Without a state filter the results mislead: an idle session that ran a quick query an hour ago still has a query_start an hour old, so it looks like a one-hour query. state = 'active' keeps only backends that are executing a statement. Sessions in idle in transaction are a separate problem and get their own query above.
Excluding background processes and itself
backend_type = 'client backend' removes autovacuum workers, WAL senders, the checkpointer and other server processes that appear in the same view. pid <> pg_backend_pid() removes the monitoring session itself, which is always active while the query runs.
Wait event columns
wait_event_type and wait_event are independent of state. An active backend may or may not be waiting; when it is active and the wait event is non-null, the statement is executing but blocked somewhere. A Lock type means the backend is waiting for a heavyweight lock, IO means it is waiting for an I/O operation to complete, and Client means it is waiting for activity on the socket connected to the application. A long query with no wait event is not reported as waiting, which moves the question to the plan itself.
Truncating the query text
left(query, 200) keeps the output readable in a terminal. The query column itself is capped by track_activity_query_size, which defaults to 1024 bytes and can be changed only at server start, so very long statements arrive truncated regardless.
Key Session States
The state column drives how every row should be read.
active
The backend is executing a query. This is the only state in which query is the statement currently running and query_start is the time it began.
idle
The backend is waiting for a new command from the client. The query column holds the previous statement. A long list of idle sessions is a connection-pool sizing question, not a slow-query one.
idle in transaction
The backend is inside a transaction but not executing a statement. Locks taken earlier remain held, so a session stuck here can block others while looking harmless in the first query. The (aborted) variant is the same, except a statement in the transaction raised an error and the transaction must be rolled back.
Other values
starting, fastpath function call and disabled also exist. disabled is reported when track_activities is turned off in that backend.
Practical Applications
The query is most useful as the first step of a short loop: find the process, read what it waits on, then decide between waiting, cancelling, and fixing.
Incident triage during a latency spike
When response times jump, run the first query and look at the top few rows. A single statement at the top with a Lock wait points at a blocker, so check which session holds the lock. Many statements with the same text and no wait event point at a plan problem or a missing index, and EXPLAIN (ANALYZE, BUFFERS) on that statement is the next step.
Cancelling a runaway report
Once the process ID is known, SELECT pg_cancel_backend(12345); cancels the current query of that backend and leaves the connection open. pg_terminate_backend() ends the whole session and is the heavier option, appropriate for a session stuck in idle in transaction. A non-superuser can signal a backend only when the caller is a member of the role that owns it or holds pg_signal_backend.
Catching blocked sessions
An active backend whose wait_event_type is Lock is waiting, not working. Sort or filter on that column to list the victims, then follow the chain to the holder with pg_blocking_pids() or a join against pg_locks.
Scheduling a repeating check
Wrapping the first query in a cron job or a monitoring probe that counts rows older than a threshold gives a cheap alert for stuck queries. Record the result with a timestamp, because the view keeps no history and a statement that finishes between two runs leaves no trace.
Version Compatibility
pg_stat_activity has changed shape across releases. Before 9.2 the process ID column was procpid and the statement text was current_query. From 9.2 the view has pid, state and a boolean waiting. From 9.6, wait_event_type and wait_event replace waiting, so a wait can be classified rather than only detected.
PostgreSQL 10 added backend_type, which lets a query separate client sessions from background processes. PostgreSQL 13 added leader_pid, which links a parallel worker to its leader, and PostgreSQL 14 added query_id, which holds an identifier that is null unless compute_query_id is enabled or a module computes one. Where that identifier is available, it can be matched against the queryid column of pg_stat_statements to move from a live session to its cumulative statistics.
The wait event names themselves change between major versions, and the list is long, so match the names against the manual for the version in use before alerting on a specific string.
Best Practices
- Grant monitoring access deliberately — give the monitoring role
pg_read_all_statsso it sees query text for every session without superuser rights. - Filter by state before reading durations — an old
query_starton an idle row is not a slow query. - Pair with timeouts —
statement_timeout(default 0, disabled) bounds a statement from the server side, and a role- or database-level setting is safer than one global value. - Log what you cannot catch live —
log_min_duration_statement(default -1, disabled) records completed statements over a threshold, which covers queries that finish before anyone looks. - Cancel before terminating —
pg_cancel_backend()stops the statement and preserves the connection; reservepg_terminate_backend()for sessions that do not respond or that sit idle in a transaction. - Keep the 30-second default honest — set the threshold from the application's latency budget, not from habit.
References
- PostgreSQL: The Cumulative Statistics System — The
pg_stat_activitycolumns, state values, wait event types and thebackend_typelist. - PostgreSQL: Run-time Statistics —
track_activity_query_sizeandcompute_query_id. - PostgreSQL: Error Reporting and Logging —
log_min_duration_statementand how completed slow statements are logged. - PostgreSQL: System Administration Functions —
pg_cancel_backend()andpg_terminate_backend()permissions. - Crunchy Data: One PID to Lock Them All — Following an active session with a lock wait back to the process that holds the lock.
Posts in this series
- Identify Blocking PostgreSQL Queries with pg_stat_activity
- PostgreSQL query to find columns containing only NULL values
- Analyze PostgreSQL Cache-Hit Ratio with pg_stat_statements
- How to Monitor Slow Running Queries in PostgreSQL
- Find Idle in Transaction Sessions in PostgreSQL
- Monitor PostgreSQL Index Build Progress with SQL
- Find PostgreSQL Tables That Need VACUUM FREEZE
- Detect PostgreSQL Transaction ID Wraparound Risk
- Cancel vs Terminate PostgreSQL Backends Explained
- Identify Running PostgreSQL Autovacuum Worker Sessions