IO_COMPLETION Wait Type in SQL Server

Part of the SQL Server Wait Types Library

IO_COMPLETION covers I/O waits that happen outside the normal buffer pool page path. Where PAGEIOLATCH_SH and PAGEIOLATCH_EX track data page reads and writes through the buffer pool, IO_COMPLETION tracks everything else: sort spills to tempdb worktables, backup file writes, restore reads, and DBCC work file I/O.

The wait type name is generic, which makes it harder to diagnose than the PAGEIOLATCH_* family. The first step is always figuring out what type of I/O is generating the wait.


What Generates IO_COMPLETION

Sort spills to tempdb, the most common cause in production. When a query does not get enough memory to complete a sort or hash operation in-memory, it spills to disk, writing and reading back worktable rows in tempdb. Each spill read and write generates IO_COMPLETION. Unlike PAGEIOLATCH_*, these are worktable pages, not buffer pool pages, so they appear as IO_COMPLETION instead.

Backup operations, writing backup data to disk (or a network share) generates IO_COMPLETION on the backup I/O threads. If backup jobs are running, elevated IO_COMPLETION is expected.

DBCC CHECKDB and DBCC CHECKTABLE, DBCC creates internal work files in tempdb for its snapshot and processing work. Large databases running DBCC will show IO_COMPLETION.

Database restore, reading backup files during a restore generates IO_COMPLETION on the restore threads.

Bulk operations using row set caches, some bulk insert paths use tempdb staging that goes through IO_COMPLETION rather than the buffer pool.


Is This Wait Expected?

During backup windows, yes. If your backup job runs at midnight and IO_COMPLETION spikes then, it is just the backup. Check the timing correlation.

During DBCC runs, yes. DBCC CHECKDB on a large database generates substantial tempdb I/O.

Outside of maintenance windows, investigate. IO_COMPLETION during production hours usually means sort spills, which means queries are not getting the memory they need (missing indexes or stale stats) or tempdb I/O is slow.


How To Identify The Cause

Check whether backup jobs are running:

SELECT
    job.name                        AS job_name,
    ja.start_execution_date,
    ja.last_executed_step_date,
    jh.run_status
FROM msdb.dbo.sysjobs job
JOIN msdb.dbo.sysjobactivity ja ON ja.job_id = job.job_id
JOIN msdb.dbo.sysjobhistory jh  ON jh.job_id = job.job_id
WHERE ja.start_execution_date IS NOT NULL
  AND ja.stop_execution_date  IS NULL
ORDER BY ja.start_execution_date DESC;

Or simply check if BACKUPIO is also elevated in the wait stats, backup operations generate both.

Find queries that are spilling to tempdb:

SELECT TOP 20
    qs.total_spills / qs.execution_count  AS avg_spills_per_exec,
    qs.total_spills,
    qs.execution_count,
    qs.total_grant_kb / qs.execution_count AS avg_grant_kb,
    qs.total_worker_time / qs.execution_count AS avg_cpu_us,
    SUBSTRING(qt.text, (qs.statement_start_offset / 2) + 1,
        ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
          ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2) + 1) AS statement_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qs.total_spills > 0
ORDER BY qs.total_spills DESC;
Get-WaitStatistics output in the dba-tools terminal and web UI

Queries with avg_spills_per_exec > 0 are consistently spilling.

Check tempdb I/O specifically:

SELECT
    mf.name                         AS file_name,
    mf.type_desc,
    mf.physical_name,
    vfs.io_stall_write / 1000.0     AS write_stall_sec,
    vfs.io_stall_read  / 1000.0     AS read_stall_sec,
    vfs.num_of_writes,
    vfs.num_of_reads,
    CASE WHEN vfs.num_of_writes > 0
         THEN vfs.io_stall_write / vfs.num_of_writes ELSE 0 END AS avg_write_ms,
    CASE WHEN vfs.num_of_reads  > 0
         THEN vfs.io_stall_read  / vfs.num_of_reads  ELSE 0 END AS avg_read_ms
FROM sys.dm_io_virtual_file_stats(2, NULL) vfs   -- database_id 2 = tempdb
JOIN sys.master_files mf
    ON mf.database_id = vfs.database_id
    AND mf.file_id    = vfs.file_id
ORDER BY vfs.io_stall_write + vfs.io_stall_read DESC;

High avg_write_ms or avg_read_ms on tempdb data files during production hours, combined with spilling queries, confirms sort spills are the cause.


What To Do

For sort spills (the most common cause):

The root cause is almost always a missing index or stale statistics. A query that spills to disk is doing work it should not need to do.

  1. Identify the spilling queries from dm_exec_query_stats above
  2. Run their execution plans in SSMS with “Include Actual Execution Plan”, look for Sort operators with a yellow warning triangle (“Warnings: Operator used tempdb to spill data”)
  3. Check for missing indexes on the tables involved:
.\run.ps1 Get-MissingIndexes
  1. Update statistics on the involved tables
  2. If the query legitimately needs to sort large data sets (e.g. a reporting query), consider whether the result set can be reduced before sorting

For tempdb I/O performance in general:
– Tempdb data files should be on fast storage, ideally the same SSD/NVMe tier as production data
– Multiple tempdb data files (one per physical core, up to 8) reduce allocation contention, though they do not directly improve sort spill I/O

For backup-driven IO_COMPLETION:
– Enable backup compression, less data written means faster backup I/O
– Schedule backups during low-traffic windows
– Consider offloading backups to an AG secondary replica

For DBCC-driven IO_COMPLETION:
– Run DBCC CHECKDB during off-peak hours
– On Enterprise Edition, DBCC with PHYSICAL_ONLY is faster and generates less tempdb I/O (though it checks less)


Related Scripts


Get The Scripts

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *