SYSTEMS OPERATIONAL
VPS

VPS Load Average High But CPU Idle? Diagnosing I/O Wait

Getwebup 6 min read

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:

CauseHow to confirmTypical fix
Swap thrashing (RAM is tight)free -h shows low free memory and active swap use; vmstat shows non-zero si/soAdd RAM, or find and stop the memory hog with ps aux --sort=-%mem
Full backup or rsync jobA tar, rsync, or backup script owns the top spot in iotopReschedule to off-peak hours, run with ionice -c3
Unindexed MySQL query or table repairmysqld shows in iotop; slow query log has repeat entriesAdd the missing index, run heavy queries off-peak
Malware/AV scan crawling every fileclamscan or similar owns disk I/O for an extended periodSchedule scans overnight, exclude large static/media directories
Noisy neighbor on shared storageNo single local process explains the wa%, especially on budget VPS plansMove 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 -n19 so 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.

#vps #io-wait #load-average #iostat #vmstat #disk-performance

Keep reading

Chat with Support