Disk I/O problems are some of the easiest Linux performance issues to misread. CPU looks fine, memory is not completely exhausted, load average is high, and the server still feels sticky. Commands hang for a second or two. Databases stutter. Backups start slowly and then drag everything else with them. That is usually when I stop staring at top and start looking at storage latency.
I/O troubleshooting is less intuitive than CPU troubleshooting because high utilization does not always mean the same thing on different storage types, and the old “disk busy percentage” mental model can lead you straight into the weeds. This guide is the workflow I actually use on Linux systems to figure out whether storage is the bottleneck, which process is causing it, and whether the problem is throughput, queueing, latency, or swap pressure.
If you are also dealing with disk capacity rather than performance, server disk space management and LVM usage are good follow-ups. Capacity and I/O pain often show up together.
Install the tools first, because some minimal images do not have them
On Debian or Ubuntu, iostat comes from sysstat and iotop is its own package.
sudo apt update
sudo apt install -y sysstat iotop
On RHEL, Rocky, or AlmaLinux:
sudo dnf install -y sysstat iotop
I do this early because too many cloud images ship without the tools I need at the moment I need them.
Start with iostat: it shows me whether storage is actually unhappy
The command I use most often is:
iostat -xz 1
Flags:
-xextended statistics-zomit devices with all-zero activity1refresh every second
That gives me a moving view instead of a one-time snapshot. The first report is since boot, so I pay more attention to the later lines.
The columns I care about most
Different versions vary a little, but these are the usual signal columns:
r/sreads per secondw/swrites per secondrkB/sandwkB/sthroughputrrqm/sandwrqm/smerged read/write requestsawaitaverage time for I/O requests to complete%utilpercentage of time the device was busy
I also watch aqu-sz or similar queue depth fields on newer versions.
What high %util means, and what it does not
People love %util because it looks authoritative. I treat it as a clue, not a verdict.
On spinning disks, %util near 100% often really does mean the device is saturated. On SSDs and especially NVMe, high %util by itself is less definitive. Modern devices can handle huge parallelism. I care more about latency and queueing than the percentage alone.
If %util is high and await is rising, users are probably feeling it.
If %util is high but latency is low and stable, the device may simply be busy doing fine.
await is where the pain usually shows up
If I had to choose one column to explain user complaints, it would often be await.
There is no universal “bad” threshold because workloads differ, but this is my rough practical feel:
- low single-digit milliseconds is usually healthy for SSD-backed workloads
- tens of milliseconds starts getting noticeable for databases and interactive applications
- hundreds of milliseconds is usually serious trouble
When await jumps during write-heavy bursts, I suspect one of these first:
- database checkpoints or flush storms
- runaway logging
- backup jobs competing with production traffic
- swap thrashing
rrqm/s and wrqm/s: merged requests are not just trivia
These columns show read and write requests merged by the kernel or device layer before being sent down. People often ignore them, but they help me understand access patterns.
Higher merge rates can be normal for sequential workloads and can improve efficiency. Very low merge rates on spinning media during random workloads can hurt performance. On SSDs the story is different, but the fields still give context.
I do not stare at rrqm/s and wrqm/s first, yet when I am comparing a healthy period to a bad one, those numbers can explain why the same throughput now causes more latency.
Use iotop to find the process causing the I/O spike
Once I know storage is busy, I want the culprit.
sudo iotop
This usually requires root privileges because it reads kernel accounting data per process.
To show only processes actively doing I/O:
sudo iotop -o
That is the mode I use most often. Otherwise a quiet system shows too much irrelevant noise.
What I look for in iotop
- a database process writing heavily during complaints
- a log shipper or app process flooding writes
- a backup or compression job unexpectedly running during business hours
- package management, antivirus, or indexing work on systems that should be quiet
One recurring lesson from production: the top I/O consumer is not always the one with the biggest business impact. A backup job may be writing steadily and legally, but the real problem is that it overlaps with the database flush window.
vmstat adds context iostat does not show alone
I almost always pair iostat with vmstat.
vmstat 1
The fields I care about most for I/O diagnosis are:
biblocks received from block devicesboblocks sent to block devicessiswap insoswap out
If si and so are moving, I pay attention immediately. Swap activity changes the whole interpretation of the system. What looks like generic disk slowness may actually be memory pressure turning into I/O pain.
Swap thrashing looks ugly in practice
Typical clues:
vmstatshows repeated swap in/outiostatshows high write activity and rising latency- the server feels uneven or frozen in bursts
topshows processes in uninterruptible sleep (Dstate)
At that point, tuning storage alone will not save you. You may need to fix memory pressure, query behavior, worker count, or caching.
dstat is useful when I want one dashboard view
Some systems still have dstat, though it is not as universal as it once was.
dstat -cdngy 1
That can show CPU, disk, network, and paging together. I would not rely on it as my only tool, but when I want quick correlation across subsystems, it is handy.
Because packaging varies, I do not assume it is present. When it is missing, I stick with iostat, vmstat, and ss or sar depending on the incident.
Historical data helps when the spike already passed
Live tools are great when the problem is happening now. They are less helpful when somebody tells me “the server was awful at 03:10” and the box is calm again by the time I log in.
That is when I use sar, which is also part of sysstat on many systems:
sar -d 1 5
And for stored daily reports:
sar -d -f /var/log/sysstat/sa22
The exact file name varies by day and distribution. The point is that historical disk counters can prove whether a slowdown was a one-off burst, a recurring backup window, or an all-day storage problem.
/proc/diskstats is the raw source when I need to get serious
Sometimes I want the kernel counters directly.
cat /proc/diskstats
This is not a friendly view, but it is the underlying source for a lot of higher-level tools. I use it when:
- I am validating a monitoring tool
- I want to script a custom comparison
- package availability is limited
A lot of people never need to read it manually, which is fine. I am mentioning it because understanding that these tools are reading counters rather than magic helps when outputs disagree.
ioping is good for quick latency checks
Throughput is not the whole story. Sometimes the complaint is “the server pauses randomly,” which is often latency rather than bandwidth.
Install if available:
sudo apt install -y ioping
Then test a mount point:
sudo ioping -c 10 /var/lib/postgresql
Or current directory:
ioping -c 10 .
This gives me a feel for responsiveness. It is not a substitute for real workload analysis, but it is a useful sanity check.
If the latency reported by ioping looks bad while iostat is relatively quiet, I start thinking about underlying storage layers outside the guest OS: noisy neighbors, SAN contention, cloud volume credits, or a hypervisor problem I cannot fix from inside the VM.
Deep block-layer analysis with blktrace and blkparse
Most incidents do not need this level. Some do.
On systems where the tools are installed, the workflow is usually:
sudo blktrace -d /dev/nvme0n1 -o - | sudo blkparse -i -
Or trace to files first if I need post-analysis:
sudo blktrace -d /dev/sda -o traceout
sudo blkparse traceout.*
I reserve this for cases where normal monitoring does not explain latency spikes and I need to see the block-layer behavior in more detail. It is powerful, but not a first-response tool.
SSDs and NVMe changed the way I read storage metrics
Old instincts from spinning disks can mislead you badly on SSD-backed systems.
What matters more on SSDs
- latency consistency
- queue depth behavior
- IOPS patterns
- write amplification under real workloads
What matters less than it used to
- simple “disk is 100% busy so it must be dead” conclusions
- sequential-vs-random fear in the old spinning-disk sense
On NVMe, I can see large IOPS counts without obvious pain, then get real trouble from bursts of latency caused by contention, garbage collection, or an overloaded virtualization layer underneath.
That is why I focus so much on await and practical user symptoms.
Choosing the I/O scheduler with intent
Schedulers are less dramatic than they used to be, but they still matter.
Check the current scheduler:
cat /sys/block/nvme0n1/queue/scheduler
You might see something like:
[none] mq-deadline kyber bfq
My rough guidance
- NVMe:
noneormq-deadlineare common sensible choices - general server workloads on block devices:
mq-deadlineoften behaves well - desktop or interactive workloads:
bfqmay help responsiveness
I do not change the scheduler during an incident unless I know exactly why I am doing it. But when tuning a persistently unhappy workload, it is worth testing.
Temporarily set one:
echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler
Remember that changes made this way do not survive reboot unless persisted through bootloader or udev/system configuration appropriate to the distribution.
Real problems I see most often
Database-heavy writes
Symptoms:
- high
w/s - growing
await - database latency complaints
iotopshows the database process at the top
What I check:
- checkpoint settings
- WAL or redo log placement
- backup overlap
- query patterns causing write amplification
Log flooding
Symptoms:
- huge write throughput from app or logging processes
- sudden log growth
- CPU may still look normal
I have seen simple bugs write thousands of lines per second and drown otherwise healthy SSDs. That is where logrotate management and application log discipline matter.
Validation commands I use:
sudo iotop -o -b -n 3
sudo du -sh /var/log/*
sudo find /var/log -type f -size +100M -ls
Those three commands usually tell me whether the “storage problem” is really just uncontrolled logs filling disks and forcing constant writes.
Swap pressure
Symptoms:
vmstatshowssiandso- system is erratic, not just slow
iotopmay showkswapdinfluence indirectly
The fix is usually memory-side: reduce workload, tune services, add RAM, or stop overcommitting.
Backup jobs colliding with production
Symptoms:
- predictable time-of-day slowdown
rsync, snapshot, compression, or database dump process near the top iniotop
This is why I schedule backups carefully and watch contention, especially when using rsync for backups on the same host as the application.
Validation commands I use before making changes
Before I tune anything, I capture a baseline.
iostat -xz 1 10
vmstat 1 10
sudo iotop -o -b -n 5
cat /sys/block/sda/queue/scheduler
The -b batch mode in iotop is useful for documentation and comparisons.
If the device mapping is unclear, I verify it:
lsblk -o NAME,TYPE,SIZE,MOUNTPOINT
findmnt
On virtualized systems, I also want to know whether the storage layer below me is shared or throttled, because guest tuning cannot fix host-side saturation.
Common mistakes during I/O troubleshooting
Looking only at load average
High load does not automatically mean CPU. Processes waiting on disk can drive load high while CPUs sit fairly idle.
Treating %util as a universal truth
Useful, yes. Sufficient, no.
Ignoring swap activity
If swap is active, you are not just debugging storage anymore.
Killing the loudest process without understanding its role
Sometimes the process doing the I/O is the database writer that exists because another service is misbehaving. The visible actor is not always the real cause.
Tuning the scheduler before identifying the workload
Scheduler tweaks can help, but they are not a substitute for finding the process and pattern actually causing latency.
My practical I/O triage flow
This is the sequence I use most:
iostat -xz 1to confirm device pressurevmstat 1to see swap and block activityiotop -oto find active processeslsblkto map devices to mounts- validate scheduler and storage type
- if needed,
iopingor deeper tracing
Example:
iostat -xz 1
vmstat 1
sudo iotop -o
lsblk -o NAME,TYPE,SIZE,MOUNTPOINT
cat /sys/block/nvme0n1/queue/scheduler
sudo ioping -c 10 /var/lib/mysql
That usually gets me to the right hypothesis quickly enough.
If the process is obvious but the timing is not, I correlate with the service manager:
systemctl list-timers --all
crontab -l
sudo ls /etc/cron.d
Scheduled jobs are responsible for more I/O spikes than people like to admit.
Conclusion
Disk I/O monitoring is really latency investigation with extra steps. iostat tells me whether the device is busy and whether waits are growing. iotop tells me who is pushing the workload right now. vmstat tells me whether memory pressure is part of the story. Everything beyond that depends on how stubborn the problem is.
The practical lesson I keep relearning is that storage bottlenecks are often workload-design problems wearing a hardware costume. A bad backup window, uncontrolled logging, swap pressure, or write-heavy query pattern can make excellent storage look terrible. If you watch await, identify the active process, and keep the workload context in mind, you solve these faster and make fewer pointless tuning changes.