VPS Load Average High But CPU Idle? Diagnosing I/O Wait
You SSH into your VPS, run uptime, and the load average is sitting at 8 on a 4-core box. Alarming. Except when you run top, the CPU columns show 80-90% idle. Nothing is actually burning CPU, yet the server feels sluggish and every command takes a beat longer than it should. This is I/O wait, and it's a different animal from a CPU-bound slowdown.
Symptom: High Load Average, Idle CPU
The giveaway is the mismatch. A CPU-bound server shows high %us or %sy in top alongside the high load average - something is genuinely computing. An I/O-bound server shows the opposite: CPU sits mostly idle, but the load average keeps climbing anyway, SSH feels laggy, ls on a busy directory takes a beat, and MySQL queries that normally return instantly start timing out or queueing.
That's because Linux's load average doesn't only count processes waiting for CPU time. It also counts processes stuck in uninterruptible sleep - usually a process waiting on a disk read or write to complete. If sixty processes are all queued behind a slow disk, your load average reflects that queue even though the CPU has nothing to do.
Step 1: Confirm It's Actually I/O Wait
Don't guess from the symptom alone - check the numbers. Run:
vmstat 1 5
Look at the wa column under CPU. That's the percentage of time the CPU spent idle while waiting on disk I/O. Anything consistently above 10-15% on a VPS is worth investigating; anything above 30% means disk I/O is your real bottleneck, not CPU.
You can see the same number inside top - press 1 to expand per-core stats, and check the wa field in the CPU summary line at the top.
Step 2: Find Which Process Is Actually Blocked
High wa% tells you disk I/O is the problem, not which process is causing it. Two ways to narrow it down:
Check for processes in D state
ps -eo pid,ppid,stat,%cpu,comm | awk '$3 ~ /D/'
D in the STAT column means uninterruptible sleep - the process is blocked waiting on I/O and can't even be killed with a normal signal until the I/O call returns. If the same PID or command shows up repeatedly across a few runs of this command, that's your culprit.
Use iotop if it's installed
sudo iotop -oa
The -o flag only shows processes actually doing I/O, and -a shows accumulated read/write totals rather than a per-second snapshot, which makes bursty offenders easier to spot. If it's not installed: apt install iotop or yum install iotop.
Cause: What Actually Drives Disk I/O Wait
A handful of things repeatedly cause this on shared or budget VPS instances:
| Cause | How to confirm | Typical fix |
|---|---|---|
| Swap thrashing (RAM is tight) | free -h shows low free memory and active swap use; vmstat shows non-zero si/so | Add RAM, or find and stop the memory hog with ps aux --sort=-%mem |
| Full backup or rsync job | A tar, rsync, or backup script owns the top spot in iotop | Reschedule to off-peak hours, run with ionice -c3 |
| Unindexed MySQL query or table repair | mysqld shows in iotop; slow query log has repeat entries | Add the missing index, run heavy queries off-peak |
| Malware/AV scan crawling every file | clamscan or similar owns disk I/O for an extended period | Schedule scans overnight, exclude large static/media directories |
| Noisy neighbor on shared storage | No single local process explains the wa%, especially on budget VPS plans | Move to an NVMe-backed plan with dedicated I/O, or ask your host to check the node |
Fix: Resolving the Bottleneck
If it's swap
Confirm with free -h and swapon --show. If swap usage is climbing steadily rather than sitting flat, you're genuinely short on RAM and the kernel is paging to disk to compensate - which on a spinning or budget SSD is painfully slow. Find the memory hog:
ps aux --sort=-%mem | head -10
Often it's PHP-FPM configured with too many workers for the available RAM, or a MySQL innodb_buffer_pool_size set larger than the box can actually support. Trim the config or add RAM - swap should be a safety net, not your working memory.
If it's a backup or scan job
Don't kill it outright if it's mid-run on something important - reschedule it and lower its I/O priority instead:
ionice -c3 -p <PID>
-c3 is the "idle" I/O class - the job only gets disk time when nothing else needs it. For cron-driven jobs, wrap the command itself: ionice -c3 nice -n19 /path/to/backup.sh, and move the schedule to your lowest-traffic hour.
If it's MySQL
Turn on the slow query log temporarily and watch for repeat offenders:
mysql -e "SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 2;"
tail -f /var/lib/mysql/$(hostname)-slow.log
A missing index on a large table is the most common cause - EXPLAIN the query and check whether it's doing a full table scan. If a table shows as needing repair, run OPTIMIZE TABLE or myisamchk during a maintenance window, not during traffic hours.
If nothing local explains it
If iotop shows no single process consuming meaningful I/O yet wa% stays high, the bottleneck may be outside your control - shared storage on the physical host being hammered by another tenant. This is more common on entry-level shared VPS plans. Open a ticket with disk I/O evidence (your vmstat output) attached; a host running you on NVMe-backed, resource-isolated storage should be able to confirm or rule this out quickly.
Prevention: Keep I/O Wait From Sneaking Back
- Set up ongoing monitoring (Netdata or similar) so you catch a rising
wa%trend before it becomes an outage, not after. - Default all backup, sync, and scan scripts to run with
ionice -c3 nice -n19so they never compete with live traffic for disk time. - Stagger cron jobs - don't let a nightly backup, a malware scan, and a database optimize job all fire in the same 15-minute window.
- Keep an eye on swap usage over time, not just at a single point - a slow upward creep means you're outgrowing your current RAM allocation.
- If your workload is genuinely I/O-heavy (large WooCommerce catalogs, big media libraries, frequent database writes), move to NVMe-backed storage rather than fighting spinning-disk or shared SATA SSD limits indefinitely.
Frequently asked questions
What's a normal load average on a VPS?
As a rule of thumb, a load average sustained at or below your core count is fine. On a 4-core VPS, a load average hovering around 4 or under is healthy; consistently above that means processes are queued, whether the bottleneck is CPU or disk I/O.
Does high load average always mean I need to upgrade my VPS?
No. Upgrading adds more CPU cores, which doesn't fix a disk I/O bottleneck. Diagnose first with vmstat and iotop - often it's one runaway backup job, a missing MySQL index, or swap thrashing from a memory-hungry process, all of which are free to fix.
What does 'D' mean in the STAT column of ps output?
D stands for uninterruptible sleep. The process is blocked waiting on a disk (or other device) I/O call to return and cannot be interrupted by normal signals, including some kill signals, until that I/O completes.
Can I just kill a process stuck in D state?
Usually not with a standard kill or even kill -9 - the process is waiting inside the kernel for I/O to finish and won't respond until it does. Fix the underlying I/O bottleneck (free up disk contention, resolve swap pressure) and the process will typically clear on its own.
How is I/O wait different from high CPU usage?
High CPU usage means processes are actively computing and competing for CPU cycles - top shows high %us or %sy. I/O wait means the CPU is sitting idle while processes queue up waiting on disk reads or writes - top shows high wa% instead. The diagnosis and fixes are different, so check vmstat before assuming it's a CPU problem.