Analyzing Wait Events and Wait Statistics
AI generated
SELECT
JOIN
SQL Debugging · Wait Events · Root Cause Analysis
Analyzing Wait Events and Wait Statistics
when CPU utilization does not explain where the time goes

A database with low CPU utilization can still respond drastically slowly, because sessions are actually waiting on locks, disk IO, or the network, not on free compute time. Wait events make exactly that invisible wait time measurable and categorizable. Systematically distinguishing lock waits from IO waits leads to the actual root cause, instead of optimizing past CPU or memory metrics.

18 min read pg_stat_activity · wait_event · Lock Waits · IO Waits PostgreSQL · MySQL · SQL Server

1. Why CPU time alone does not explain where the time goes

A classic diagnostic mistake is treating the database server's CPU utilization as the primary indicator of performance problems. A database can still respond drastically slowly at ten percent CPU utilization, because most sessions are not actually computing at all, they are waiting: on a lock held by another transaction, on a disk operation that has not completed yet, or on a network response from a replication partner. From the perspective of CPU and memory metrics, the system looks relaxed, even though requests are actually hanging for seconds.

Wait events close exactly that gap by recording, for every session, what it is currently waiting on and for how long. Instead of only knowing that a query is slow, a wait event shows the concrete category of the wait time, for example lock wait time, IO wait time, or wait time on an internal lightweight lock. This categorization turns a vague observation, the system is slow, into a concrete, actionable diagnosis of which resource is causing the bottleneck.

This shift in perspective, from aggregated system metrics to a session based view of wait time, is the actual core of professional database diagnosis. Classic metrics like CPU utilization, free memory, or IOPS describe the state of the entire server, but say nothing about which individual query or which individual use case is currently slow for a particular user. Wait events close exactly that gap between server metric and user experience, by breaking wait time down to the individual session and the individual moment.

2. What wait events actually measure

A wait event is a timestamp and a categorization that a database engine maintains for every process as soon as it is not actively consuming compute time but waiting on an external condition. In PostgreSQL, this state becomes visible through the columns wait_event_type and wait_event in pg_stat_activity, where wait_event_type gives the broad category, for example Lock, IO, or LWLock, and wait_event names the specific event within that category. If wait_event is instead NULL, the session is currently in an actively executing state and is not waiting on anything external, an important distinction to keep in mind when filtering queries against this view.

SQL Server follows a conceptually similar but more granular model through sys.dm_os_wait_stats, which collects cumulative wait times per wait type across all sessions since the last restart or reset. Oracle uses the Active Session History (ASH) mechanism, a sampling based approach that records the state of every active session, including its wait event, once every second. All three approaches pursue the same goal: making visible what a session's time is actually spent on, instead of relying on aggregated system metrics.

3. Key wait event categories overview

For practical diagnosis it is enough to focus on a handful of frequently occurring categories. Lock waits indicate a session waiting for a lock held by another transaction, classically caused by concurrent UPDATE or DELETE operations on the same rows. IO waits, in PostgreSQL for example DataFileRead, indicate a session waiting on physical disk reads, often a sign of a buffer cache that is too small or missing indexes causing excessive disk access.

LWLock waits (lightweight locks) in PostgreSQL concern internal synchronization mechanisms of the database engine itself, for example access to shared memory structures, and typically only show up at very high concurrency. Client waits indicate the database waiting on the client, usually because the application consumes data more slowly than the database can deliver it, a clue that shifts the cause from the database into the application.

A fifth, less commonly discussed category is replication wait time, for example WalSenderWaitForWAL in PostgreSQL, which indicates a session waiting for acknowledgment from a synchronous replication partner before a transaction counts as complete. This category is often overlooked because it only occurs in configurations with synchronous replication, but there it can make up the dominant wait time for write operations, especially when the replication partner is connected over a slow or unstable network link.

4. Querying wait statistics live

The first practical step is querying the current state of every active session along with its wait event. In PostgreSQL, a targeted query against pg_stat_activity, filtered to sessions with a set wait_event, provides a snapshot of what the system is currently waiting on. Repeating this query once a second over a short window creates a simple but effective sampling profile that makes the dominant wait categories visible, without any additional monitoring software.

In SQL Server, sys.dm_os_waiting_tasks provides the same live snapshot, augmented with blocking_session_id, which directly shows which session holds the lock another session is waiting on. That field is especially valuable because it makes the detour through a separate lock join, necessary in PostgreSQL, unnecessary, combining blockers and waiters into a single query.


-- PostgreSQL: snapshot of what every active session is currently waiting on
SELECT
    pid,
    usename,
    wait_event_type,
    wait_event,
    now() - query_start AS waiting_since,
    left(query, 60) AS current_query
FROM pg_stat_activity
WHERE wait_event IS NOT NULL
ORDER BY query_start;

-- SQL Server: live view of waiting tasks including the blocking session
SELECT
    wt.session_id,
    wt.wait_type,
    wt.wait_duration_ms,
    wt.blocking_session_id,
    st.text AS current_query
FROM sys.dm_os_waiting_tasks wt
JOIN sys.dm_exec_requests r ON r.session_id = wt.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) st
ORDER BY wt.wait_duration_ms DESC;

5. Distinguishing lock waits from IO waits

The distinction between lock waits and IO waits directly determines the direction further diagnosis and remediation should take. A system dominated by lock waits suffers from contention between transactions, the fix typically lies in shorter transactions, see the earlier article on long running transactions, a changed table access order, or optimistic instead of pessimistic locking. A system dominated by IO waits, on the other hand, suffers from physical disk access, the fix tends to lie in more memory for the buffer cache, missing indexes, or faster storage hardware.

The practical mistake many teams make is reflexively adding more indexes for every performance problem, which stays ineffective, or even worsens the situation, for a problem primarily caused by lock waits, because additional indexes increase write load per transaction and thereby hold locks longer. Wait event categorization prevents this fallacy by first identifying the actual resource causing the bottleneck before choosing a fix.

Another practical benefit of this distinction shows up when prioritizing optimization work in teams with limited capacity. If wait event analysis shows a system suffering eighty percent from lock waits, spending time on code review to shorten transaction boundaries pays off far more than spending time on hardware upgrades for faster storage, which does not address the actual problem at all. This data driven prioritization prevents budget from flowing in the wrong direction just because one measure seems more obvious or easier to implement than the other.


-- PostgreSQL: aggregate wait events by type over a sampling window
-- Run this query every second for a few minutes and sum the counts
SELECT
    wait_event_type,
    wait_event,
    count(*) AS sample_count
FROM pg_stat_activity
WHERE wait_event IS NOT NULL
GROUP BY wait_event_type, wait_event
ORDER BY sample_count DESC;

-- A result dominated by 'Lock' entries points to transaction contention
-- A result dominated by 'IO' entries points to disk or cache pressure

6. Collecting and evaluating historical wait data

A single live snapshot only shows the current moment, while many performance problems occur intermittently and have already disappeared again by the time you check manually. That is why continuously collecting wait statistics over time pays off, for example through a simple cron script that runs the query from section four once a second and writes it into a dedicated table, or through established extensions like pg_stat_kcache combined with pg_stat_statements.

SQL Server already provides a cumulative view since the last restart through sys.dm_os_wait_stats, which is useful for trend analysis but does not allow time resolution of individual incidents. Query Store, available since SQL Server 2016, is the better choice for that purpose, because it stores wait statistics per query and time interval, showing retroactively which query suffered under which wait category at which point in time, without having to observe the incident in real time.

7. From wait events to root cause analysis

Wait events alone provide a categorization but not yet a complete explanation. The next step connects the dominant wait category with further context: for lock waits, a join against the lock views, as shown in the article on long transactions, provides the concretely blocking session. For IO waits, a look at the buffer pool's cache hit ratio, computable in PostgreSQL from pg_stat_database, indicates whether too little memory is available for the application's working set.

Only combining the wait event category, the affected table or query, and time correlation with deployments or load spikes yields a complete root cause analysis. A proven approach is to answer three questions for every significant performance incident: which wait event category dominated, which concrete sessions or queries were involved, and what changed in the system or in a deployment immediately before it.

This three part question structure also works well as a documentation template for post mortem reports after a performance incident. Instead of a vague description like "the database was slow", answering the three questions produces a concrete, traceable finding, for example "ninety percent lock waits on the orders table, caused by session 4821, correlated with the deployment of version 2.14 at 2:32 PM". Such a finding translates directly into a corrective action and simultaneously serves as a reference should a similar pattern occur again.

8. Typical wait event patterns and what they mean

Certain patterns repeat often enough in practice to become established rules of thumb. A sudden spike in lock waits shortly after a deployment usually points to a new, longer running transaction in the changed code. A consistently high share of IO waits throughout the entire day points to a structurally too small buffer cache relative to database size, not a temporary problem.

A rise in LWLock waits specifically at very high concurrency, for example during a sales event with many simultaneous connections, points to internal contention within the database engine itself, often solvable through connection pooling on the application side to limit the number of concurrent database connections. These patterns do not replace a detailed case by case analysis, but they provide a quick first clue as to which direction further investigation should take.

Another recurring pattern involves client waits that regularly spike at certain times of day. This often points to a batch job or report export that slowly fetches large result sets over a poorly configured network connection or an undersized application server, while the database itself has already finished computing and is only waiting for the data to be picked up. This pattern deliberately shifts optimization out of the database and toward network tuning or application side batching of result processing.

9. Comparing wait event types across databases

The concrete terminology and available tools for wait event analysis differ between the common systems. The following table compares the key concepts.

System Live view Historical view Lock context readily available
PostgreSQL pg_stat_activity Custom sampling required No, join against pg_locks required
SQL Server sys.dm_os_waiting_tasks Query Store Yes, blocking_session_id directly
Oracle v$session with wait_class Active Session History (ASH) Partially, via blocking_session
MySQL / InnoDB performance_schema.events_waits_current performance_schema history tables Via sys.innodb_lock_waits

SQL Server and Oracle offer the more mature native tools for historical wait event analysis with Query Store and Active Session History respectively, while PostgreSQL and MySQL rely more heavily on external extensions or custom sampling to reach the same level of retrospective detail.

For teams operating several database systems in parallel, a unified conceptual model in their own documentation pays off despite these differences: every wait category gets classified, independent of the concrete system, as a lock, IO, internal synchronization, or client wait before looking up the system specific query. This shared vocabulary makes it easier for teams managing both PostgreSQL and SQL Server to compare diagnostic findings across systems, without relearning terminology every time they switch.

Mironsoft

Wait event analysis, root cause diagnosis and database performance

Low CPU utilization, yet still slow queries?

We analyze wait events and wait statistics, separate lock contention from IO bottlenecks, and deliver a solid root cause analysis instead of guessing from CPU metrics.

Live diagnosis

Evaluating current wait events and blocking sessions in real time

Historical sampling

Continuous recording of wait statistics for trend analysis

Root cause report

Concrete causes and prioritized actions instead of vague recommendations

Before the summary closes out the article, a brief look back at the whole diagnostic chain is worthwhile: from observing low CPU utilization despite noticeable slowness, through live querying of current wait events, to distinguishing lock dominated from IO dominated bottlenecks, and finally historical recording for recurring or intermittent incidents. Each of these steps builds on the previous one and delivers increasingly concrete courses of action.

Anyone who has systematically worked through this chain once establishes a reusable mental model for future performance incidents, regardless of the concrete database system in use. Terminology may vary between PostgreSQL, MySQL, SQL Server, and Oracle, but the underlying principle, categorizing wait time instead of merely observing symptoms, stays identical and universally applicable across every system.

10. Summary

Wait events close the gap that pure CPU and memory metrics leave open, by making visible what every session is concretely waiting on when it is not actively computing. Distinguishing lock waits, IO waits, and internal LWLock waits directly determines which direction remediation should take, from shorter transactions to more buffer cache or better connection pooling.

A single live snapshot is often not enough, because many problems occur intermittently, which is why continuous sampling or native tools like Query Store and Active Session History are indispensable for a solid root cause analysis. Anyone who consistently combines wait event category, affected queries, and time correlation with deployments replaces guesswork with a reproducible, data driven diagnosis.

In the end, a simple but effective principle applies: before implementing an optimization, the dominant wait event category should be known, not merely assumed. This discipline prevents time from being invested in measures that miss the actual cause, for example adding more memory to a problem actually caused by lock contention, or the other way around.

As a final practical note: wait event analysis does not replace the specific diagnostic techniques covered in earlier articles, such as reading EXPLAIN plans or tracking down long transactions, it complements them with an overarching, session based perspective. In practice, the order that works well is determining the dominant category through wait events first, and only then diving deeper with whichever specialized tool fits that category.

This combination of broad categorization and targeted detail analysis is ultimately why experienced database administrators use wait events as the first point of call for every new performance incident, regardless of how the incident was initially reported.

For newcomers to this topic, a good starting point is running the simplest live query from section four regularly against your own production database, even without an acute incident. Simply observing which wait event categories are common during normal operation builds an intuitive sense of what is normal and what stands out as a deviation in an actual emergency.

As experience grows, the focus increasingly shifts from pure observation toward proactive prevention, for example through automated threshold alerts for specific wait event categories, long before a user ever notices a slowdown.

This exact transition from reactive troubleshooting to proactive monitoring marks the difference between a team constantly firefighting and a team that catches performance problems before they ever become an incident.

Analyzing wait events and wait statistics, the essentials

Core idea

Wait events show what a session is waiting on, not just that it is slow, and make wait time categorizable.

Key distinction

Lock waits point to transaction contention, IO waits point to disk or cache bottlenecks.

Live diagnosis

pg_stat_activity or sys.dm_os_waiting_tasks provide the current snapshot.

Historical analysis

Continuous sampling or Query Store and Active Session History for past incidents.

11. FAQ: Wait Events and Wait Statistics

1What is a wait event?
A categorization of how a session is waiting when not actively computing, such as on a lock or a disk operation.
2Why isn't CPU utilization enough?
A database can be slow at low CPU load because sessions wait on locks, IO, or network instead of computing.
3How do I see wait events in PostgreSQL?
Via pg_stat_activity with wait_event_type and wait_event, filtered to set values.
4Lock waits vs. IO waits?
Lock waits show wait time on locks, IO waits show wait time on disk access, both need different fixes.
5Why don't more indexes fix lock waits?
More indexes increase write load and can hold locks longer instead of solving the problem.
6What does Query Store offer?
Wait statistics per query and time interval, for retrospective analysis of past incidents.
7What are LWLock waits?
Internal synchronization mechanisms of the database engine, occurring at very high concurrency.
8How do I collect historical wait data?
A cron script running the query every second and storing it, since PostgreSQL offers no native historical view.
9What does blocking_session_id show?
Directly which session holds the lock, no separate lock join needed.
10What does a spike after deployment mean?
Usually a new, longer running transaction in the changed code, recognizable by time correlation.