Wait event metrics
Active session history and wait event metrics from pg_stat_activity.
Data source
Wait event data is collected from pg_stat_activity:
select
pid,
state,
wait_event_type,
wait_event,
query
from pg_stat_activity
where backend_type = 'client backend';
Core metrics
Series are exported with the pgwatch_ prefix. Wait-event sampling counts come from the
wait_events metric group (column total), and session state/duration come from the
pg_stat_activity group.
Session state metrics
| Metric | Type | Description |
|---|---|---|
pgwatch_pg_stat_activity_count | Gauge | Sessions, grouped by state |
pgwatch_pg_stat_activity_max_tx_duration | Gauge | Longest running transaction (seconds) |
Wait event metrics
| Metric | Type | Description |
|---|---|---|
pgwatch_wait_events_total | Gauge | Sampled count of backends per wait event / type |
There is no pg_wait_event_count or pg_wait_event_activity_count series.
Labels
Session state labels (pgwatch_pg_stat_activity_count)
| Label | Values | Description |
|---|---|---|
state | active, idle, idle in transaction, idle in transaction (aborted), ... | Session state |
datname | Database name | Target database |
application_name | Application name | Reported application_name |
Wait event labels (pgwatch_wait_events_total)
| Label | Values | Description |
|---|---|---|
wait_event_type | LWLock, Lock, BufferPin, Activity, Client, Extension, IPC, Timeout, IO, plus the synthesized CPU* placeholder (see below) | Wait category |
wait_event | Various | Specific wait event |
datname | Database name | Target database |
query_id | queryid (PG14+) | Associated query id, when available |
Wait event categories
CPU* (synthesized placeholder)
CPU is not a real PostgreSQL wait_event_type. When a backend is running on CPU, its
wait_event and wait_event_type are NULL. To make on-CPU activity visible, the wait_events
metric coalesces those NULLs into a CPU* placeholder value — so you will see CPU* (not CPU)
in the wait_event / wait_event_type labels. The categories below are the standard PostgreSQL
wait-event types reported as-is.
IO
Disk I/O operations:
| Event | Description |
|---|---|
DataFileRead | Reading data file blocks |
DataFileWrite | Writing data file blocks |
DataFileExtend | Extending data file |
DataFileFlush | Flushing data file |
WALRead | Reading WAL |
WALWrite | Writing WAL |
WALSync | Syncing WAL to disk |
Lock
Row and table-level locks:
| Event | Description |
|---|---|
relation | Table lock wait |
tuple | Row lock wait |
transactionid | Transaction lock wait |
virtualxid | Virtual transaction lock |
LWLock
PostgreSQL internal lightweight locks:
| Event | Description |
|---|---|
buffer_content | Buffer content lock |
buffer_mapping | Buffer mapping lock |
WALInsert | WAL insertion lock |
lock_manager | Lock manager lock |
ProcArray | Process array lock |
BufferPin
Buffer pinning waits:
| Event | Description |
|---|---|
BufferPin | Waiting for buffer pin |
Activity
Background process waits:
| Event | Description |
|---|---|
LogicalLauncherMain | Logical replication launcher |
AutoVacuumMain | Autovacuum launcher |
BgWriterMain | Background writer |
CheckpointerMain | Checkpointer process |
WalWriterMain | WAL writer |
Common queries
Sessions by state
sum by (state) (pgwatch_pg_stat_activity_count)
Wait events distribution
sum by (wait_event_type) (pgwatch_wait_events_total)
Active (non-idle) sessions
sum(pgwatch_pg_stat_activity_count{state!~"idle.*"})
Sessions waiting on locks
sum(pgwatch_wait_events_total{wait_event_type="Lock"})
I/O wait ratio
sum(pgwatch_wait_events_total{wait_event_type="IO"})
/
sum(pgwatch_pg_stat_activity_count{state="active"})
Long-running transactions
pgwatch_pg_stat_activity_max_tx_duration > 300
Dashboard usage
These metrics are used in:
- 01. Node overview — Active session history
- 04. Wait events — Wait event deep-dive
- 13. Lock contention — Lock analysis
Average Active Sessions (AAS) health matrix
The PostgresAI console rolls this wait-event data up into its DB health matrix as Average Active
Sessions (AAS) checks: an overall check plus one per wait-event category (IO, IPC, Lock, LWLock).
Each check pairs a sustained avg value with a peak value. The peak is derived from
pgwatch_wait_events_total — the worst single time bucket, plus a p99 across buckets — and
normalized to the instance's vCPU count, so a cell reads the same regardless of machine size.
Peak granularity: 1-minute buckets
The peak metrics are computed over 1-minute buckets:
- The zone is driven by the worst 1-minute bucket (previously the worst 5-minute bucket).
- The displayed p99 label is the p99 of 1-minute buckets (previously p99 of 5-minute buckets).
A 1-minute maximum catches short bursts that 5-minute averaging smooths away, surfacing sub-5-minute CPU-queuing spikes that the coarser bucket hid. The avg (sustained) checks are unaffected — only the peak metrics change.
Thresholds are absolute and resolution-invariant
The peak zone cutoffs are unchanged, and intentionally so:
| Peak check | Green | Yellow | Red |
|---|---|---|---|
Overall (AASp) | < 50% of vCPUs | 50–80% | > 80% |
Per type (AAS_IOp, AAS_IPCp, AAS_LOCKp, AAS_LWLOCKp) | < 20% of vCPUs | 20–50% | > 50% |
These cutoffs are absolute and deliberately not raised for the finer granularity. The worst minute exceeding 80% of vCPUs means real CPU queuing regardless of bucket size, so the threshold is resolution-invariant. Raising the cutoffs to compensate for 1-minute granularity would either under-alert spiky workloads or re-smooth the very sub-5-minute bursts that 1-minute buckets exist to surface.
The switchover is a calibration boundary
Because a 1-minute maximum catches bursts that 5-minute averaging smooths, worst/peak values computed over 1-minute buckets run greater than or equal to the old 5-minute values for the same workload. At the switchover:
- Peak cells may shift redder even though the underlying workload has not changed.
- Peak history from before the switch is not directly comparable to peak history after it.
Treat the switchover as a calibration boundary: compare peaks within a single granularity regime, not across it. The avg/sustained values are continuous across the boundary.
Troubleshooting
No wait event data
-
Verify pgwatch is collecting from pg_stat_activity:
docker compose logs pgwatch-postgres pgwatch-prometheus | grep -i "stat_activity" -
Check monitoring user permissions (the product grants
pg_monitor, notpg_read_all_stats):grant pg_monitor to postgres_ai_mon;
Wait events show "unknown"
Some wait events may not be captured if the session state changes between sampling intervals. This is normal for very short waits.
High "idle in transaction" count
Indicates connection leaks or application issues:
select pid, usename, state, query_start, query
from pg_stat_activity
where state = 'idle in transaction'
order by query_start;
Related metrics
- pg_stat_statements — Query-level metrics
- Table metrics — Table access patterns