Load average is the most widely quoted and most frequently misread metric in Linux monitoring. It appears in every dashboard, it triggers every alert rule someone copied from a blog post, and it is wrong about CPU saturation roughly half the time. On a VPS the confusion is worse, because steal time and I/O wait inflate load without any local process doing work. This guide covers what load average actually counts, where it lies, and what to monitor instead.
What load average actually counts
Load average is not CPU utilisation. It is the size of the run queue plus the count of tasks in uninterruptible sleep, exponentially averaged over 1, 5, and 15-minute windows. Two contributors:
- Runnable tasks — threads that want CPU time and are waiting for it. This is the part people expect.
- Uninterruptible tasks (D state) — processes blocked on I/O or on a lock that cannot be interrupted. This is the part that breaks the mental model. A server doing nothing but waiting on a slow disk can show a load average of 20 with 4% CPU utilisation.
The convention of comparing load to core count assumes contributor one dominates. On any VPS where storage is the bottleneck — which is most of them — that assumption fails.
The two failure modes that produce identical load numbers
| Scenario | Load (4 vCPU VPS) | CPU % | iowait % | Actual user impact |
|---|---|---|---|---|
| CPU-bound (image resize, crypto) | 4.2 | 99 | 1 | Slow, but throughput scales with queueing |
| I/O-bound (slow disk, DB flush) | 4.2 | 6 | 78 | Requests time out while CPU looks idle |
| Steal-bound (noisy neighbour) | 3.8 | 15 | 5 | Everything slow; nothing you can tune locally fixes it |
All three produce roughly the same load figure and require completely different responses. This is the argument for never alerting on load average alone.
The metrics that actually tell you about CPU saturation
1. Pressure Stall Information (PSI)
This is the modern, direct answer. PSI reports the percentage of wall-clock time that tasks were stalled waiting for a resource.
cat /proc/pressure/cpu
# some avg10=42.18 avg60=31.55 avg300=18.02 total=981234567
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
For CPU, the some value represents tasks that were runnable but had to wait. On a busy but healthy web server, some in the 10–25% range means there is contention but throughput is fine. Above 50% average, users are measurably waiting for CPU. full for CPU is rarely non-zero because it requires every task to be stalled at once.
Compare all three resource types side by side:
for r in cpu memory io; do
echo "== $r"; head -1 /proc/pressure/$r
done
# Or read the whole picture in one command
systemctl show user.slice -p MemoryPressureWatch -p CPUPressureWatch 2>/dev/null
PSI resolves the ambiguity in the table above in a single glance: high io pressure with low cpu pressure means you have an I/O problem that load average has disguised as a CPU problem.
2. Actually measure utilisation per core, not per system
A 4-core VPS with one pinned single-threaded process shows 25% total utilisation and is completely saturated. Per-core view:
mpstat -P ALL 2 5
Then separate idle time from time the CPU wanted to work but could not, by inspecting the whole CPU line in top or vmstat:
vmstat 2 10
# Columns that matter: r (run queue), b (blocked), us, sy, id, wa, st
The st column is steal time — CPU cycles the hypervisor gave to another tenant. It is the most important column on a VPS and the one that does not exist on bare metal. Anything above 5% sustained means local tuning has a hard ceiling; see our steal-time diagnostics guide for how to confirm and escalate it.
3. Run queue length as a ratio, not an absolute
The r column in vmstat is the instantaneous run queue. The useful normalisation is runnable threads divided by available CPUs. From within a container or VM, get the correct CPU count with nproc, not lscpu, which may report the physical host’s cores in some virtualisation setups.
getconf _NPROCESSORS_ONLN
nproc --all
# Run-queue-to-core ratio sampled over 10 minutes:
vmstat 5 120 | awk 'NR>2 && $1 ~ /^[0-9]+$/ { printf "%.3f\n", $1/' \
| awk '{ s+=$1; n++ } END { printf "avg run queue: %.2f\n", s/n }'
A ratio under 1.0 means no CPU queueing. Between 1 and 2 means some jobs wait occasionally. Persistently above 3 means the workload genuinely needs more vCPU — but only if the pressure and per-core utilisation numbers agree. If the run queue is long while cores are 70% idle, you are in D state, and the queue is an I/O artifact.
A decision procedure you can run in five commands
# 1. Is there CPU contention at all?
cat /proc/pressure/cpu
# 2. Is the contention actually I/O?
cat /proc/pressure/io
# 3. Are cores saturated, or is one core pegged?
mpstat -P ALL 2 5
# 4. Is the hypervisor taking cycles?
vmstat 2 10 # look at column st
# 5. Is the run queue long with idle cores?
vmstat 2 10 # r vs id mismatch = D-state tasks
Interpretation in four lines:
cpu somehigh, cores saturated,stnormal → genuine CPU shortage → optimise code or add vCPU.io somehigh,cpu somelow, run queue long, cores idle → I/O bottleneck → fix storage, not CPU.stabove 5% sustained → noisy neighbour → provider issue, not a tuning issue.- One core at 100%, total at 25% → single-threaded bottleneck → fix the application’s parallelism, not the plan.
Setting alert thresholds that do not wake you up for nothing
Replace the classic “load > cores” rule with a small set of rules that encode the distinctions above:
| Signal | Warning | Critical | Why |
|---|---|---|---|
/proc/pressure/cpu some avg60 | > 30% | > 60% | Direct measure of tasks waiting for CPU |
/proc/pressure/io full avg60 | > 2% | > 8% | Every task stalled on storage |
vmstat st column | > 5% | > 15% | Hypervisor contention |
| Load / nproc (15-min avg) | > 2 | > 5 | Only meaningful combined with the above |
If you use Prometheus and node_exporter, PSI is exposed as node_pressure_cpu_waiting_seconds_total and the equivalents for memory and I/O. The rate over a 5-minute window gives you exactly the per-resource stall percentage shown in the table. Alerting on rate against a fixed threshold is far less noisy than alerting on a gauge load average.
The specific case of containerised VPS environments
Two extra traps when your VPS runs containers or is itself a container:
- Load average is host-wide unless you are in a PID namespace with proper accounting. Inside an unprivileged container,
/proc/loadavgmay reflect the host. Check whether your reported load correlates with your own traffic at all; if it does not, you are reading someone else’s number. - cgroup v2 pressure files are per-slice. If you have bounded your application with a CPU quota, read the pressure files under
/sys/fs/cgroup/<slice>/cpu.pressurerather than the system-wide ones. A service throttled by its own quota shows high slice pressure with an idle system — that is a quota sizing problem, and it is covered in our systemd cgroups v2 resource control article.
# Confirm which CPU count your cgroup actually sees
cat /sys/fs/cgroup/cpu.max # e.g. "200000 100000" = 2 CPUs
cat /sys/fs/cgroup/cpu.pressure
cat /sys/fs/cgroup/cpu.stat # nr_throttled / throttled_usec
nr_throttled climbing is definitive proof that your own cgroup is the constraint — no amount of host capacity would help until the quota changes.
Summary
Load average is a queue length that conflates runnable work with I/O-blocked work. On a VPS it is further distorted by steal time. Use it as a coarse triage signal, then resolve the ambiguity with PSI pressure files, per-core utilisation, and vmstat‘s st and r columns. The reward is alerting that distinguishes “add a vCPU” from “fix your disk” from “call your provider” — three problems with three different owners and three different budgets.
Whichever branch the diagnosis lands in, two numbers are worth confirming before you change a plan: the RAM allotment and whether steal time is contractually bounded. InterServer publishes both on its VPS plans, which makes the CPU-speed branch of this decision a calculation rather than a guess. If the plan you land on needs no local tuning at all, Cloudways’ managed hosting keeps the monitoring layer handled and lets you spend the effort on the application instead. For the wider context on how memory and CPU guarantees differ between tiers, our sysctl kernel tuning guide covers the settings that only work when the underlying allocation is honest.

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