Troubleshoot High I/O Wait in Linux

Tested on RHEL 10.2 (Coughlan) — vm1.lab.example (3 logical CPUs)
Package procps-ng 4.0.4-11.el10
sysstat 12.7.6-4.el10
util-linux 2.40.2-18.el10
stress 1.0.7-5.el10_0
Applies to Ubuntu, Debian, Kali Linux, Linux Mint, Pop!_OS, Raspberry Pi OS, elementary OS, Zorin OS, Parrot OS, MX Linux, RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora, Arch Linux
Privilege Normal user for top, mpstat, iostat, and vmstat; sudo or root for pidstat -d on all processes and swap inspection
Scope Diagnose Linux high iowait using mpstat, top, iostat, pidstat, D state checks, and vmstat swap columns. Does not cover full disk I/O monitoring tool reference or filesystem tuning.
Related guides High load average troubleshooting
High CPU usage troubleshooting
Monitor disk I/O performance
iostat command
vmstat command

top shows 56.7 wa in the CPU line while %usr stays in single digits — CPUs are idle, but they are idle because they are waiting on disk, not because the host has spare capacity. That wa column is I/O wait, and it is one of the first signals that a storage bottleneck is slowing applications.

The workflow below confirms iowait is really elevated, names the busy device with iostat, ties writes to a PID with pidstat, checks for uninterruptible D state blockers, and rules out swap thrash before you resize disks or blame application code.


What I/O wait means

I/O wait (%iowait in mpstat, wa in top and vmstat) is the fraction of CPU time where a processor had nothing to run because tasks were waiting on block I/O to finish. The CPU is not executing user or kernel code during that slice — it is parked until the storage stack returns.

I/O wait is not the same as disk utilization. A disk can be busy at %util 100 while %iowait is moderate if enough work stays in CPU cache or if I/O completes quickly. Conversely, high %iowait with rising w_await in iostat means applications are stalling on slow or saturated storage.

I/O wait also differs from load average. Load includes tasks in uninterruptible sleep (D state) that may not show as high %iowait on every sample. Use both metrics together — see high load average troubleshooting when load and iowait diverge.


Confirm I/O wait is high

Start with mpstat from the sysstat package. It prints %iowait per interval so you catch brief spikes a single top snapshot might miss:

bash
mpstat 1 3

On an idle lab host, %iowait stays near zero:

output
Average:     all    0.17    0.00    0.33    0.00    1.50    0.33    0.00    0.00    0.00   97.66

The fifth numeric column after %sys is %iowait; the last column is %idle. Values above roughly twenty percent sustained while users report lag deserve a storage investigation on most servers.

top in batch mode shows the same signal in the summary line — look for wa:

bash
top -b -n 1 | head -5

During a heavy sync-write test on this VM, wa climbed past fifty percent:

output
%Cpu(s):  3.3 us,  3.3 sy,  0.0 ni, 30.0 id, 56.7 wa,  6.7 hi,  0.0 si,  0.0 st

Low us with high wa is the classic iowait pattern — the box is not CPU-starved; it is disk-starved. Re-run mpstat or top during the incident window, not after users say performance recovered.


Identify the busy storage device

Once %iowait is elevated, find which block device is saturated. iostat -xz prints per-disk stats and hides idle devices:

bash
iostat -xz 1 3

During stress --hdd on the lab host, dm-0 and sda showed full utilization:

output
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          10.53    0.00   84.21    0.00    0.00    5.26

Device            r/s     rkB/s     w/s     wkB/s  w_await  aqu-sz  %util
dm-0            53.00    212.00 2244.00 294956.00     1.97    4.49 100.00
sda             53.00    212.00 2290.00 292396.00     1.77    4.12   90.70

%util near one hundred on sda / dm-0 pinpoints local disk pressure. On systems with NFS, iSCSI, or multipath, map the busy /dev node back to mounts with lsblk and findmnt before chasing the wrong LUN.

The avg-cpu header in the same iostat output also lists %iowait — useful when you want disk and CPU breakdown on one screen.


Check latency with iostat

Utilization alone does not tell you whether latency is acceptable. Read w_await and r_await (milliseconds per I/O) and aqu-sz (average queue depth):

bash
iostat -xz 1 2

Under sustained writes in the lab, queue depth and await rose with utilization:

output
Device            r/s     w/s  w_await  aqu-sz  %util
dm-0           793.07 405315.35     3.74    2.97   98.61
sda            989.11 406752.97     4.80    4.82   86.24

w_await of a few milliseconds on SSD-backed VMs is normal; double-digit or triple-digit await on production arrays under load explains application timeouts. Compare against your storage baseline — a sudden jump matters more than an absolute number.

If %iowait is high but every local disk shows low %util, look at network storage mounts, hypervisor datastore contention, or RAID rebuild activity before assuming the tool is wrong.


Find processes generating I/O

pidstat -d attributes read and write throughput to processes. Run it while %iowait is elevated:

bash
pidstat -d 1 3

During stress --hdd, the stress workers dominated disk writes:

output
Average:      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
Average:        0     23198      0.00 712589.47 522778.95       0  stress
Average:        0     23199      0.00 637978.95 522778.95       0  stress

kB_wr/s and kB_rd/s are kilobytes per second per process. High kB_ccwr/s means cache-flushing writes (fsync-heavy workloads). Note the PID, then confirm the command line:

bash
ps -p 23198 -o pid,user,stat,etime,cmd
output
PID USER     STAT     ELAPSED CMD
  23198 root     D          00:08 stress --hdd 2 --hdd-bytes 512M --timeout 15

Parallel dd jobs with oflag=direct produced a similar pattern — several dd PIDs with hundreds of megabytes per second in pidstat. Short cron bursts may appear only in pidstat loops, so sample for at least thirty seconds during the slowdown.


Check processes in D state

Processes in D state (uninterruptible sleep) wait on kernel I/O and cannot be killed until the driver returns. They inflate load average and often accompany storage stalls:

bash
ps -eo state,pid,cmd | awk '$1 ~ /D/ {print}'

During the stress --hdd run on the lab host:

output
D   23198 stress --hdd 2 --hdd-bytes 512M --timeout 15
D   23199 stress --hdd 2 --hdd-bytes 512M --timeout 15

Occasional kworker lines in D during heavy flush are common. Many application PIDs stuck in D for minutes, especially with NFS mounts, point at hung storage or network paths — not a simple CPU tuning problem.

vmstat column b counts blocked processes waiting for I/O:

bash
vmstat 1 3
output
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  3      0 1281924   5052 4163932    0    0     4 266208 3273 2200  0 46 31 22  0
 0  2      0 1242324   5052 4206980    0    0     0 260596 1260  897  0 16 39 45  0

Rising b with high wa and large bo (blocks written out) matches a write-heavy bottleneck. bi/bo show system-wide block I/O rates in kilobytes per interval.


Check swap activity

Swap traffic is disk I/O. When memory is exhausted, paging drives %iowait on the swap device even if application disks look idle:

bash
free -h
output
Mem:           7.5Gi       2.6Gi       1.9Gi       5.3Mi       3.3Gi       4.9Gi
Swap:          3.0Gi          0B       3.0Gi

On this lab VM swap is unused (0B used). Production hosts with swap in use need vmstat swap columns:

bash
vmstat 1 3

Watch si (swap in) and so (swap out). Non-zero sustained values mean the kernel is paging — relieve memory pressure before tuning disk schedulers. See check memory usage per process when free shows little available memory and %iowait spikes without an obvious write workload.

List active swap devices with:

bash
swapon --show
output
NAME      TYPE      SIZE USED PRIO
/dev/dm-1 partition   3G   0B   -2

If swap I/O correlates with iowait on dm-1, add RAM or reduce process footprint before buying faster application disks.


Identify the I/O bottleneck

Match the dominant signal to the fix — adding CPU rarely helps when %iowait and disk %util lead the chart.

What you see Likely bottleneck Next step
High %iowait, disk %util ~100, high w_await Saturated local disk or LUN Spread data, faster storage, reduce sync writes, schedule backups off-peak
High %iowait, many D PIDs, NFS mounts Network filesystem stall Check NFS server and network; review mount options
High %iowait, high si/so, swap in use Memory pressure → swap I/O Add RAM; tune app memory; reduce cache footprint
High %iowait, low disk %util Remote or hypervisor storage latency Check SAN fabric, datastore contention, cloud volume limits
High bo in vmstat, specific PID in pidstat -d One runaway writer Stop or throttle that job; fix log rotation or batch size
High %iowait only on one app server Not a kernel bug by default Compare iostat on DB vs app; trace dependency chain

After you address the root cause, confirm %iowait dropped:

bash
mpstat 1 2
output
Average:     all    0.17    0.00    0.33    0.00    1.50    0.33    0.00    0.00    0.00   97.66

Return to idle-level %iowait within a few minutes of stopping a heavy writer is the recovery pattern you want. For ongoing disk monitoring beyond incident response, continue with monitor disk I/O performance and the iostat command reference.


Troubleshooting

Symptom Likely cause Fix
%iowait high, %util low on all disks NFS/iSCSI latency or mis-mapped device Trace mount to array; check path and switch errors
One PID in D forever Hung driver or stuck NFS hard mount Fix storage path; remount with appropriate options
%iowait spikes at same time daily Cron backup or log rotation Reschedule; use ionice; write to separate volume
High iowait after RAID rebuild starts Expected rebuild I/O Wait or throttle rebuild; monitor iostat await
pidstat shows no writer, iowait still high Short-lived processes or kernel flush Longer pidstat sample; check dmesg for errors
iowait high after kernel upgrade Storage driver regression Roll back or patch; test with vendor tools

References


Summary

High I/O wait on Linux means CPUs are idle while tasks wait on block storage — not that you need more processor cores. Confirm the spike with mpstat or top during the incident, then use iostat -xz to see which device hits high %util and whether w_await latency explains the lag.

pidstat -d ties megabytes per second to a PID so you can distinguish a runaway backup from kernel flush noise. Processes in D state and a rising vmstat b column point at blocking I/O; swap-in and swap-out traffic means memory pressure is masquerading as a disk problem.

Use the bottleneck table to pick storage tuning, mount fixes, or RAM — not all three at once. When load is high but the story is unclear, cross-check high load average and high CPU usage guides so you do not chase the wrong resource.


Frequently Asked Questions

1. What is a high iowait value on Linux?

There is no universal threshold — context matters. On a database or file server, sustained %iowait above 20–30 in mpstat or top while applications lag usually means CPUs are waiting on disk. On an idle host, %iowait near zero is normal. Compare iowait to disk %util and await in iostat before adding CPU.

2. How do I find which process causes high iowait?

Run pidstat -d 1 5 during the slowdown and read kB_rd/s and kB_wr/s per PID. Cross-check with iostat -xz for the busy device, then ps -p PID -o user,cmd and /proc/PID/cmdline. Short-lived jobs may only appear in pidstat, not in a one-shot ps snapshot.

3. Why is load average high but iowait is low?

Load counts runnable and D-state tasks, not only disk wait time. High load with low %iowait often means CPU-bound work or many threads in the run queue — see high CPU usage troubleshooting. High load with high %iowait and D-state processes points at storage.

4. Does high iowait always mean the disk is broken?

Not necessarily. Heavy legitimate writes, fsync-heavy databases, RAID rebuilds, backups, and NFS stalls all raise iowait. Check iostat %util and w_await on the specific device. One saturated LUN on a shared array can starve the host even when CPU is idle.

5. Can swap cause high iowait?

Yes. When memory is tight, the kernel pages data to swap on disk — every swap-in and swap-out is I/O. Watch vmstat si and so columns and free -h. Rising swap traffic with high %iowait on the swap device means relieve memory pressure first, not only tune the application.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)