Load average is the single most misread number on a VPS. It counts runnable and uninterruptible (D-state) tasks over 1, 5 and 15 minutes — it does not measure CPU utilisation, and it is not normalised by core count. A load of 4 on a 2-vCPU box is saturated; a load of 4 on an 8-vCPU box is healthy. This article shows how to read the pair together so you stop chasing the wrong metric.
What load average actually counts
- Runnable threads waiting for CPU (
Rstate). - Threads in uninterruptible sleep (
D) — usually blocked on disk or NFS, not CPU. - Load is not divided by cores; you must do that yourself.
The killer ambiguity: D-state threads inflate load while the CPU sits 30% idle. Seeing load climb with low CPU means you have an I/O problem, not a CPU problem.
Separate the two sources
# 1-min load and core count
cat /proc/loadavg
nproc
# who is actually runnable vs blocked
vmstat 1 5
# r = runnable (needs CPU); b = blocked (waiting on I/O)
# per-CPU utilisation
mpstat -P ALL 1 3Read vmstat columns as: high r with high us/sy = CPU saturation. High b with high wa = I/O saturation. High r with modest CPU use and rising cs = too many context switches, often a threaded app making cheap decisions too often.
| Signal | Likely cause | Next command |
|---|---|---|
| r > cores, us+sy ~100% | CPU bound | perf top, see CPU hotspot profiling |
| b > 0, wa high | Disk I/O wait | iostat -x 1, iotop |
| r high, us low, cs high | Context-switch thrash | pidstat -w 1 |
| steal (st) > 0 | Noisy neighbour / host contention | mpstat, understand CPU steal time |
That last row is specific to virtualised hosts and never appears on bare metal: CPU time stolen by the hypervisor shows as st in mpstat and explains load you cannot account for. If you are budgeting cores for a workload, the CPU steal diagnostic walkthrough shows how to confirm host-side contention before you size up.
Normalising load for core count
# quick saturation ratio: load1 / nproc
awk '{print "load1/ncpu =", $1/'$(nproc)'}' /proc/loadavg
# > 1.0 sustained => queueing; > 2.0 => severeUse the ratio, not the raw number, in alerts. A threshold of load > 4 is meaningless without knowing nproc. Define alerting as (load1 / ncpu) > 1.0 for 5 min, then split the follow-up by whether wa or us+sy dominates.
Building a saturation-based alert
# PSI gives a direct 'some tasks are stalled' signal (Linux 4.20+)
cat /proc/pressure/cpu
cat /proc/pressure/io
# avg10=45.00 => 45% of the last 10s had tasks stalled on that resourcePressure Stall Information is a better alert input than load because it directly measures the fraction of time work was delayed. Load tells you a queue exists; PSI tells you how much it is hurting. For a low-core instance, wire both into monitoring and treat load as context, not the trigger.
Put together: check nproc, look at vmstat to split runnable from blocked, use PSI to quantify stalls, and only then reach for CPU profiling. That ordering prevents the common mistake of optimising application code when the real constraint was disk latency or a noisy neighbour.
Worked example: two servers, same load, different problems
Server A runs 2 vCPUs at load 6 with mpstat showing 95% user time. Server B runs 4 vCPUs at load 6 with 40% user time and a large wa. Same load number, opposite diagnoses: A is CPU-bound and needs either fewer expensive requests or more cores; B is I/O-bound and needs faster storage or better caching. Acting on the raw load figure alone would send you down the wrong path on at least one of them.
| Instance | load1/nproc | us+sy | wa | Diagnosis |
|---|---|---|---|---|
| A (2 vCPU) | 3.0 | 95% | 1% | CPU saturated — profile the hot path |
| B (4 vCPU) | 1.5 | 40% | 35% | I/O bound — inspect disk and swap |
Only after the split do you pick a remediation. Profile CPU with perf and flame graphs for A; check iostat -x for queue depth and utilisation, and swap activity, for B. The metric that tells you which is the ratio of runnable to blocked work, not the headline load.
A monitoring checklist that avoids the trap
- Alert on
load1/nprocsustained above 1.0, never on raw load. - Always graph CPU-user, CPU-steal, iowait and PSI io side by side with load.
- Record
nprocper host so dashboards can normalise automatically. - Treat a rising load with flat CPU as an I/O or steal investigation, not a CPU one.
Load average is useful — it is a cheap early signal that a queue is forming. What it is not is a verdict on CPU. Read it next to core count, blocked-time and steal, and it becomes a reliable pointer to the right subsystem rather than a number that sends you optimising the wrong thing.

Leave a Reply
You must be logged in to post a comment.