Quick Answer
PAGEIOLATCH_EX occurs when a thread needs exclusive access to a buffer page that is currently undergoing an I/O operation (either a read or a write). This is a storage performance issue indicating slow disk I/O. High waits typically signal storage subsystem bottlenecks requiring immediate attention.
Root Cause Analysis
PAGEIOLATCH_EX waits occur when a thread requests an exclusive latch on a buffer page that is currently involved in a physical I/O operation (a read or a write). The exclusive latch is requested because the thread intends to modify the page. If the page is currently being read from or written to disk, the thread must wait until the I/O completes before it can acquire the exclusive latch and proceed.
The buffer pool manager coordinates this through the buffer descriptor's latch mechanism. When a page is undergoing I/O, SQL Server places a latch on the buffer header. Other sessions requesting exclusive access to the same page queue behind this latch until the I/O completes and the latch releases.
This differs from PAGEIOLATCH_SH waits where the thread only needs shared (read) access. PAGEIOLATCH_EX indicates a thread needs exclusive access to a page currently undergoing I/O, making it more critical because it blocks subsequent modifications entirely.
SQL Server 2016 introduced improvements to checkpoint algorithms that can reduce PAGEIOLATCH_EX waits through more efficient batching. SQL Server 2019 enhanced this further with indirect checkpoint optimizations and better coordination between lazy writer threads.
Storage latency directly correlates with PAGEIOLATCH_EX duration. SSDs typically show sub-millisecond waits while traditional spinning disks often exhibit 10-50ms waits during heavy write operations. Azure SQL Database and SQL Managed Instance show different patterns due to storage architecture differences.
AutoDBA checks Storage performance monitoring, checkpoint configuration, and buffer pool optimization across your entire SQL Server instance in 60 seconds. Download the free diagnostic script and see what else needs attention.
Diagnostic Queries
-- Current PAGEIOLATCH_EX waits and duration
SELECT
session_id,
wait_type,
wait_time_ms,
blocking_session_id,
wait_resource,
command,
cpu_time,
total_elapsed_time
FROM sys.dm_exec_requests
WHERE wait_type = 'PAGEIOLATCH_EX'
ORDER BY wait_time_ms DESC;
-- Historical PAGEIOLATCH_EX wait statistics
SELECT
waiting_tasks_count,
wait_time_ms,
max_wait_time_ms,
signal_wait_time_ms,
wait_time_ms - signal_wait_time_ms AS resource_wait_time_ms,
CAST(100.0 * wait_time_ms / SUM(wait_time_ms) OVER() AS DECIMAL(5,2)) AS pct_total_waits
FROM sys.dm_os_wait_stats
WHERE wait_type = 'PAGEIOLATCH_EX'
AND waiting_tasks_count > 0;
-- Buffer pool pressure and page life expectancy
SELECT
counter_name,
cntr_value,
CASE
WHEN counter_name = 'Page life expectancy' AND cntr_value < 300 THEN 'Critical'
WHEN counter_name = 'Page life expectancy' AND cntr_value < 600 THEN 'Warning'
ELSE 'OK'
END AS status
FROM sys.dm_os_performance_counters
WHERE object_name LIKE '%Buffer Manager%'
AND counter_name IN ('Page life expectancy', 'Checkpoint pages/sec', 'Lazy writes/sec');
-- Database file I/O performance
SELECT
DB_NAME(vfs.database_id) AS database_name,
mf.physical_name,
vfs.num_of_writes,
vfs.io_stall_write_ms,
CASE WHEN vfs.num_of_writes = 0 THEN 0
ELSE vfs.io_stall_write_ms / vfs.num_of_writes
END AS avg_write_stall_ms,
mf.type_desc
FROM sys.dm_io_virtual_file_stats(NULL, NULL) vfs
JOIN sys.master_files mf ON vfs.database_id = mf.database_id
AND vfs.file_id = mf.file_id
WHERE vfs.num_of_writes > 0
ORDER BY avg_write_stall_ms DESC;
-- Memory pressure indicators
SELECT
type,
name,
pages_kb / 1024 AS memory_mb,
CAST(100.0 * pages_kb / SUM(pages_kb) OVER() AS DECIMAL(5,2)) AS pct_total
FROM sys.dm_os_memory_clerks
WHERE pages_kb > 0
ORDER BY pages_kb DESC;
Fix Scripts
Immediate Relief: Force Checkpoint to Reduce Dirty Pages
Forces checkpoint to flush dirty pages and reduce immediate pressure. Use during maintenance windows only.
-- Force checkpoint on current database to reduce dirty page pressure
-- WARNING: This blocks other operations until complete
DECLARE @db_name NVARCHAR(128) = DB_NAME();
DECLARE @sql NVARCHAR(500) = 'CHECKPOINT';
PRINT 'Forcing checkpoint on database: ' + @db_name;
EXEC sp_executesql @sql;
-- Verify checkpoint completion (SQL Server 2017+)
SELECT
database_id,
recovery_model_desc,
log_since_last_checkpoint_mb
FROM sys.dm_db_log_stats(DB_ID());
Optimize Checkpoint Interval for Write Smoothing
Reduces checkpoint frequency to spread writes over longer periods.
-- Increase recovery interval to reduce checkpoint frequency
-- Current setting first, then change
SELECT
name,
value,
value_in_use,
description
FROM sys.configurations
WHERE name = 'recovery interval (min)';
-- Increase to 10 minutes (test in dev first)
-- This reduces checkpoint frequency but increases potential recovery time
EXEC sp_configure 'recovery interval (min)', 10;
RECONFIGURE WITH OVERRIDE;
-- Verify change
SELECT
name,
value,
value_in_use
FROM sys.configurations
WHERE name = 'recovery interval (min)';
Enable Indirect Checkpoint for Better Control
Provides more granular control over checkpoint behavior, available SQL Server 2012+.
-- Enable indirect checkpoint for current database
-- TARGET_RECOVERY_TIME controls checkpoint frequency
DECLARE @db_name NVARCHAR(128) = DB_NAME();
DECLARE @sql NVARCHAR(500);
-- Set target recovery time to 120 seconds
SET @sql = 'ALTER DATABASE [' + @db_name + '] SET TARGET_RECOVERY_TIME = 120 SECONDS';
PRINT 'Enabling indirect checkpoint with 120 second target recovery time';
EXEC sp_executesql @sql;
-- Verify setting
SELECT
name,
target_recovery_time_in_seconds,
CASE WHEN target_recovery_time_in_seconds > 0 THEN 'Yes' ELSE 'No' END AS indirect_checkpoint_enabled
FROM sys.databases
WHERE name = @db_name;
AutoDBA generates fix scripts like these automatically, with impact estimates and rollback SQL included.
Prevention
Configure storage with appropriate IOPS capacity for peak write workloads. Plan for 3-5x normal throughput during maintenance operations and index rebuilds. Use Storage Spaces Direct or enterprise SAN with adequate write cache for on-premises deployments.
Monitor Page Life Expectancy continuously, maintaining values above 300 seconds minimum. Values below this threshold indicate memory pressure forcing excessive page evictions. Scale memory before storage becomes the bottleneck.
Implement indirect checkpoint on SQL Server 2012+ with TARGET_RECOVERY_TIME between 60-120 seconds. This provides better write smoothing than traditional automatic checkpoints. Avoid setting below 60 seconds as this creates excessive checkpoint overhead.
Schedule maintenance operations during low-activity periods. Index rebuilds, statistics updates, and DBCC operations generate substantial write activity. Stagger these operations across databases and use ONLINE operations where possible.
Configure TempDB properly with multiple data files sized equally. TempDB allocation storms contribute significantly to PAGEIOLATCH_EX waits. Use trace flag 1117 and 1118 on pre-2016 versions for better allocation patterns.
Set up proactive alerting when PAGEIOLATCH_EX waits exceed 20ms average or represent more than 10% of total wait time. These thresholds indicate storage performance degradation requiring investigation.
Need hands-on help?
Dealing with persistent pageiolatch_ex issues across your environment? Samix Technology provides hands-on SQL Server performance consulting with 15+ years of production DBA experience.