Monitor Running Queries in PostgreSQL using pg_stat_activity
Monitor Running Queries in PostgreSQL (9.2+ and Newer)
A query that has been running for forty minutes looks identical, from a load balancer's perspective, to one that finished in forty milliseconds — nothing in a typical connection-pool dashboard flags the difference between a session doing real, expected work and one stuck behind a lock or waiting on a slow disk. pg_stat_activity is where PostgreSQL keeps the answer current: one row per backend, updated in real time, showing exactly what every connected session is doing right now, how long it has been doing it, and who is running it. Reading it on a schedule turns a vague "the database feels slow" report into a specific PID, user, and SQL statement.
Overview
When managing a PostgreSQL database, one of the most important tasks for database administrators and developers is monitoring currently running queries. This helps identify problematic sessions, troubleshoot performance bottlenecks, and optimize long-running queries. The query uses PostgreSQL's built-in view pg_stat_activity to list all active queries.
The state column is what makes that filtering possible. A backend's state is always one of a small, fixed set of values: active (currently executing a query), idle (waiting for the next command from the client), idle in transaction (inside a transaction block but not currently running a statement), idle in transaction (aborted) (a transaction that hit an error and is waiting to be rolled back), fastpath function call, or disabled (when track_activities is turned off for that session). Filtering state != 'idle' in the sample query below removes the majority of connections in a typical pool — the ones simply waiting for work — and leaves the ones actually worth looking at. It's worth remembering that pg_stat_activity is a live snapshot, not a log: a query that finished a second ago is already gone from the view, which is why long-running-query alerting has to poll rather than query once and assume the picture is complete.
Purpose of the Query
This query helps database administrators:
- Identify active queries running in PostgreSQL.
- Detect long-running queries that may indicate performance issues.
- Troubleshoot query locks, blocking sessions, or resource-heavy operations.
- Exclude the monitoring query itself (by filtering out references to
pg_stat_activity).
That last point matters more than it looks. Any monitoring query issued against pg_stat_activity is itself a row in pg_stat_activity while it runs, and without the NOT ILIKE '%pg_stat_activity%' filter, every scheduled health check would show up as one more "active" query cluttering its own output. The same self-exclusion logic is worth applying to any wrapper script or dashboard job that polls this view on a fixed interval — otherwise the monitoring tool becomes noise in its own report.
Sample Code from Command Line
1SELECT
2 pid,
3 age(clock_timestamp(), query_start),
4 usename,
5 application_name,
6 query
7FROM
8 pg_stat_activity
9WHERE
10 state != 'idle'
11 AND
12 query NOT ILIKE '%pg_stat_activity%'
13ORDER BY
14 query_start DESC;
Notes: pg_stat_activity and the state column used above have been available since PostgreSQL 9.2. Only superusers, members of the pg_monitor role, or a session's own user can see the full query text for a row belonging to another user — everyone else sees the text replaced with <insufficient privilege>, so a monitoring role needs one of those grants to be useful.
Filtered variant, for queries that have been running longer than five minutes:
1SELECT
2 pid,
3 usename,
4 application_name,
5 age(clock_timestamp(), query_start) AS duration,
6 state,
7 query
8FROM
9 pg_stat_activity
10WHERE
11 state != 'idle'
12 AND query NOT ILIKE '%pg_stat_activity%'
13 AND clock_timestamp() - query_start > interval '5 minutes'
14ORDER BY
15 duration DESC;
Blocking-session variant, using pg_blocking_pids():
1SELECT
2 pid,
3 usename,
4 wait_event_type,
5 wait_event,
6 query,
7 pg_blocking_pids(pid) AS blocked_by
8FROM
9 pg_stat_activity
10WHERE
11 cardinality(pg_blocking_pids(pid)) > 0;
Breakdown & Key Points
pid→ The process ID of the backend running the query. Useful for terminating queries with pg_terminate_backend(pid).age(clock_timestamp(), query_start)→ Shows how long the query has been running. This helps spot queries consuming excessive execution time.usename→ Username of the session owner running the query.application_name→ Identifies the client application connected to PostgreSQL (e.g., psql, PgAdmin, or a custom app).query→ The actual SQL text being executed by the session.state != 'idle'→ Ensures only active queries are displayed (ignores idle connections).query NOT ILIKE '%pg_stat_activity%'→ Prevents including this very monitoring query in the results.ORDER BY query_start DESC→ Orders results by start time, showing the most recent queries first.clock_timestamp() - query_start > interval '5 minutes'→ The threshold predicate in the filtered variant; swap the interval to match whatever duration counts as "too long" for a given workload.cardinality(pg_blocking_pids(pid)) > 0→ Keeps only rows where the backend is currently waiting on at least one other session, turning a full activity dump into a short blocking-only list.wait_event_type/wait_event→ Report what class of resource a waiting backend is stuck on — a lock, a lightweight lock, client I/O, or disk I/O — and the specific named event within that class.
Key Insights & Use Cases
- Performance Monitoring — Quickly detect slow queries running for a long time, which may block other transactions.
- Troubleshooting Client Applications — Identify which users or applications are causing heavy loads.
- Database Administration — Track down sessions that need to be terminated when they consume too many resources or lock critical tables.
- Query Optimization — By regularly monitoring queries, you can collect evidence on what queries should be indexed, rewritten, or cached.
- Capacity and Connection Pool Planning — A recurring pattern of many concurrent
activerows against the same table points to a workload that has outgrown its current pool size or indexing strategy, well before it shows up as a user-facing timeout.
Run any of these as a one-off during an incident, or wrap the filtered variant in a scheduled job that writes results to a log table — the second approach turns a single diagnostic query into a searchable history of exactly when a given table or query pattern started causing trouble.
Version Compatibility
The base query works unmodified on PostgreSQL 9.2 and every release since — pid, query_start, usename, application_name, query, and state have been stable columns for the entire supported version range. The blocking-session variant needs more recent server versions: PostgreSQL 9.6 replaced the old boolean waiting column with the wait_event_type and wait_event pair used above, and added the pg_blocking_pids() function as the standard way to ask "who is this session waiting on" without hand-writing a self-join against pg_locks.
PostgreSQL 10 added the backend_type column, which separates ordinary client connections from background processes such as autovacuum workers and WAL senders — useful when a monitoring query needs to exclude PostgreSQL's own maintenance activity rather than just filtering by state. The same release introduced the pg_monitor predefined role referenced in the Sample Code notes above, letting a monitoring account see every session's query text without a full superuser grant.
PostgreSQL 14 added a query_id column to pg_stat_activity, populated when compute_query_id is enabled. It correlates a currently running statement directly with its aggregate row in pg_stat_statements — a live query and its historical average cost can be looked up with the same identifier instead of matching on normalized query text.
Practical Applications
The same view supports both a quick manual check during an incident and a standing job that runs unattended.
Diagnosing a Sudden Latency Spike
When response times climb without a matching deploy or traffic change, running the filtered five-minute variant is usually the fastest way to find the cause. A handful of active rows all touching the same table, all started around the same time, points at a query plan regression or a missing index far faster than reading application logs.
Building a Blocking-Session Alert
The blocking-session query pairs naturally with pg_locks for deeper investigation once pg_blocking_pids() has identified which sessions are involved. Scheduling the blocking variant every minute and alerting on any non-empty result catches lock pileups — a batch job holding a row lock while an unrelated report query waits behind it — before they cascade into a connection-pool exhaustion incident.
Safe Session Termination Workflow
Once a problem pid is identified, pg_cancel_backend(pid) stops the current query while leaving the session connected, and pg_terminate_backend(pid) closes the connection outright. Trying the softer cancel first, and confirming the query and usename columns actually match the intended target before running either function, avoids accidentally killing an unrelated session that happens to share a similar duration.
Best Practices
- Use this query as a foundation for building monitoring dashboards.
- Automate alerts for long-running queries (e.g., > 5 minutes).
- Combine with PostgreSQL's pg_locks view when troubleshooting deadlocks or lock waits.
- Consider lightweight monitoring tools like pg_stat_statements for aggregate query statistics.
- Grant
pg_monitor, not superuser, to monitoring accounts — it exposes query text and statistics views without handing out full administrative rights. - Poll on a fixed interval rather than once —
pg_stat_activityhas no history; a query that finished between two manual checks leaves no trace in the view itself. - Prefer
pg_cancel_backend()beforepg_terminate_backend()— canceling the query is less disruptive than dropping the whole connection, and often solves the problem without an application-side reconnect.
References
- PostgreSQL Documentation — pg_stat_activity — full column reference for the view, including state values and privilege rules for query text visibility.
- PostgreSQL Documentation — pg_locks — the companion view for inspecting individual lock grants and lock types held by a blocking session.
- PostgreSQL Documentation — pg_stat_statements — the extension for aggregate, historical query statistics beyond the point-in-time view of pg_stat_activity.
- PostgreSQL Wiki — Lock Monitoring — community-maintained queries and patterns for investigating blocking and lock contention.
Posts in this series
- How Many Connections Can Your PostgreSQL Database Handle?
- PostgreSQL Backend Connections via pg_stat_database
- pg_blocking_pids — Find Blocking Queries in PostgreSQL
- List PostgreSQL Databases by Size with Access Check
- Assess PostgreSQL Database Sizes Quickly and Easily
- Unveiling Your PostgreSQL Server - A Diagnostic Powerhouse
- Keep Your PostgreSQL Database Clean, Identify Idle Connections
- Query the PostgreSQL Configuration
- pg_is_in_recovery — Monitor PostgreSQL Standby Status
- ALTER SEQUENCE RESTART WITH in PostgreSQL — Examples
- Monitor Running Queries in PostgreSQL using pg_stat_activity
- Monitor PostgreSQL Active Sessions with pg_stat_activity
- PostgreSQL Error Handling Settings via pg_settings
- PostgreSQL File Location Settings Query via pg_settings
- PostgreSQL Lock Management Settings via pg_settings
- PostgreSQL Logging Configuration Query via pg_settings
- Monitor PostgreSQL Memory Settings with pg_settings
- PostgreSQL Table Row Count Estimates with SQL
- List PostgreSQL Tables by Size with SQL
- PostgreSQL WAL Settings Query Guide
- log_parser_stats, log_planner_stats, log_executor_stats — PostgreSQL
- PostgreSQL SSL Settings Query Guide
- PostgreSQL Resource Settings Query Guide
- PostgreSQL Replication Settings Query Guide
- PostgreSQL Query Planning Settings Query Guide
- PostgreSQL Preset Options Settings Query Guide
- PostgreSQL Miscellaneous Settings Query Guide
- Count PostgreSQL Sessions by State with SQL
- Kill Idle PostgreSQL Sessions with SQL
- GRANT SELECT on All Tables in PostgreSQL — with Examples
- pg_stat_user_tables — Find Insert-Only Tables in PostgreSQL
- Detect Soft Delete Patterns in PostgreSQL
- List PostgreSQL Object Comments with SQL
- List Foreign Key Constraints in PostgreSQL
- List PostgreSQL Enum Types and Their Values with SQL
- List All Views in a PostgreSQL Database with SQL
- Find PostgreSQL Tables Without a Primary Key
- List PostgreSQL Partitioned Tables with SQL
- List All Schemas in Your PostgreSQL Database
- pg_stat_database — Query PostgreSQL Database Statistics
- List PostgreSQL Roles and Their Privileges
- Scrubbing Email PII in PostgreSQL for GDPR Compliance
- List Installed Extensions in PostgreSQL
- List Collations in Your PostgreSQL Database
- PostgreSQL Replica Identity for Logical Replication
- Monitor PostgreSQL Vacuum Progress with pg_stat_progress_vacuum
- Monitor PostgreSQL Wait Events Using pg_stat_activity
- Monitor PostgreSQL Replication Lag with pg_stat_replication
- List PostgreSQL Wait Events with the pg_wait_events View
- PostgreSQL Column-Level Permissions Audit Query
- List All PostgreSQL Triggers with Their State
- timestamptz and tzdata: Avoid Shifted PostgreSQL Timestamps
- Inspect PostgreSQL Sequences with the pg_sequences View
- List PostgreSQL Functions with pg_proc
- Query PostgreSQL Tablespace Info with pg_tablespace
- Audit PostgreSQL Authentication with pg_hba_file_rules
- List PostgreSQL Materialized Views with pg_matviews