Reading load average on multi-core machines: the three misconceptions and the alert that actually works
Load 8 is quiet on a 16-core and a disaster on a 2-core. Load also counts processes waiting on a disk, not just CPU. And the 1-minute value is almost always noise. What the three numbers really mean, how to normalise them per core, how to separate CPU pressure from I/O pressure with /proc/pressure, and which threshold to put in an alert.
Contents
- Misconception 1: "load is CPU usage"
- Misconception 2: "load above 1 is bad"
- Misconception 3: "the 1-minute value is the current one"
- The better source: /proc/pressure (PSI)
- The alert that actually works
- What do you do when the alert fires?
- Pitfalls
- What you still don't have
- How monsys does it
- FAQ
load average: 6.12, 5.87, 4.90 — now what? Most dashboards show those three numbers prominently and most administrators have a feeling about them that's roughly right. "Roughly" is the problem: the load average isn't a percentage, on Linux it counts more than just CPU, and on a 32-core machine it means something completely different than on a VPS with 2 vCPUs. This article turns it into a number you can interpret, and an alert that doesn't fire every night at 03:00.
Misconception 1: "load is CPU usage"
The load average is the average number of processes that are runnable (want CPU, have it or are waiting for it) plus, on Linux, processes in uninterruptible sleep (state D: waiting on a disk, NFS, a swap-in). That second part is a Linux quirk from 1993 and the source of most confusion.
cat /proc/loadavg
# 6.12 5.87 4.90 3/1187 481223
# │ │ │ │ │ └─ last PID
# │ │ │ │ └─ total number of threads
# │ │ │ └─ runnable right now
# └────┴────┴─ 1, 5, 15 minutes (exponentially damped average)
# Who contributes? Runnable (R) and uninterruptible (D) threads, now:
ps -eo state,pid,comm | awk '$1=="R"||$1=="D"' | sort | uniq -c | sort -rn | head
If you see many D processes, your load is an I/O story, not a CPU story — and more cores solve nothing.
Misconception 2: "load above 1 is bad"
Load is absolute, not relative. On a machine with N cores, load N is the point where every core has exactly one task. So always divide by the core count:
nproc # 8
awk -v c="$(nproc)" '{printf "load per core: %.2f %.2f %.2f\n", $1/c, $2/c, $3/c}' /proc/loadavg
# load per core: 0.77 0.73 0.61
Rules of thumb for the 15-minute figure per core:
| Load per core (15 min) | Meaning |
|---|---|
| < 0.7 | Headroom |
| 0.7 – 1.0 | Fully used, no queue — fine for a batch server, tight for a web server |
| 1.0 – 2.0 | Structural queue: every task waits on one other on average |
| > 2.0 | Overloaded, or I/O blockage (check D state) |
Mind containers and VMs: nproc inside a container returns the host's core count, but your cgroup may only be allowed two. cat /sys/fs/cgroup/cpu.max shows the quota (200000 100000 = 2 cores).
Misconception 3: "the 1-minute value is the current one"
The three numbers are exponentially damped averages. The 1-minute value reacts fastest, but therefore also includes every apt update, every logrotate and every cron that grabbed all cores for three seconds. For decisions you look at the 15-minute value; for "is something happening right now" you don't look at load but at the queue itself:
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st gu
# 9 2 0 812344 102212 4211200 0 0 120 3400 2100 5100 71 12 9 8 0 0
r = runnable threads (compare with nproc), b = blocked on I/O, wa = percentage of time CPUs wait on I/O, st = steal (your hoster gives you less CPU than promised — on a VPS the first thing to look at).
The better source: /proc/pressure (PSI)
Since kernel 4.20, Linux tracks per resource what percentage of the time tasks are waiting — CPU, memory and I/O separately. That's exactly the question load tries to answer, but without the mixing.
cat /proc/pressure/cpu
# some avg10=4.21 avg60=3.87 avg300=2.10 total=81234567
cat /proc/pressure/io
# some avg10=18.40 avg60=12.31 avg300=6.02 total=...
# full avg10=9.11 avg60=5.20 avg300=2.40 total=...
cat /proc/pressure/memory
some = percentage of time at least one task waited; full = all tasks waited (the machine stalled on that resource). An io full avg60 of 9% means: 9% of the last minute nobody did anything because the disk couldn't keep up. That's the alert you want — not "load > 4".
PSI is on by default on Ubuntu 24.04 and Debian 12. If /proc/pressure is missing, psi=1 isn't in the kernel command line.
The alert that actually works
Three rules, in order of reliability:
cpu some avg300 > 25— a quarter of the last 5 minutes something waited for CPU. Structural, not a spike.io full avg300 > 5— the machine stalled completely on I/O 5% of the time. Disk, NFS or swap.load15 / nproc > 2— the classic safety-net rule, only if PSI isn't available.
sudo tee /usr/local/sbin/pressure-check.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
NTFY="https://ntfy.example.be/servers"; HOST=$(hostname -s); MSG=()
psi() { awk -v k="$1" -v w="$2" '$1==k { for(i=2;i<=NF;i++) if ($i ~ "^"w"=") { sub(w"=","",$i); print $i } }' "/proc/pressure/$3"; }
if [ -r /proc/pressure/cpu ]; then
c=$(psi some avg300 cpu); i=$(psi full avg300 io); m=$(psi full avg300 memory)
awk -v v="$c" 'BEGIN{exit !(v>25)}' && MSG+=("cpu pressure some/5m = ${c}%")
awk -v v="$i" 'BEGIN{exit !(v>5)}' && MSG+=("io pressure full/5m = ${i}%")
awk -v v="$m" 'BEGIN{exit !(v>2)}' && MSG+=("memory pressure full/5m = ${m}% (swap/OOM near)")
else
l=$(awk '{print $3}' /proc/loadavg); c=$(nproc)
awk -v l="$l" -v c="$c" 'BEGIN{exit !(l/c>2)}' && MSG+=("load15/core = $(awk -v l="$l" -v c="$c" 'BEGIN{printf "%.2f", l/c}')")
fi
# Steal: the hoster isn't giving you your CPU. Find the column by name: newer procps adds "gu" (guest) after "st".
st=$(vmstat 1 2 | awk 'NR==2{for(i=1;i<=NF;i++) if($i=="st") c=i} END{print $c}')
[ "${st:-0}" -gt 10 ] && MSG+=("cpu steal ${st}% — hoster overcommit")
[ ${#MSG[@]} -gt 0 ] && printf '%s\n' "${MSG[@]}" | curl -s -H "Title: pressure@$HOST" -H "Priority: high" --data-binary @- "$NTFY" >/dev/null
EOF
sudo chmod 0755 /usr/local/sbin/pressure-check.sh
echo '*/5 * * * * root /usr/local/sbin/pressure-check.sh' | sudo tee /etc/cron.d/pressure-check
What do you do when the alert fires?
| Symptom | First command | Typical cause |
|---|---|---|
CPU pressure high, r > nproc | top -o %CPU, pidstat 1 | A runaway process, a cron at the wrong hour, too low a worker limit |
I/O pressure high, many D | iostat -xz 1, iotop -o | Backup, database vacuum, logrotate with compression, full disk, failing disk |
| Memory pressure high | free -m, vmstat columns si/so | Swapping; a memory leak not quite OOM yet |
| Steal high | nothing local | Your hoster. Ticket or migrate. |
Pitfalls
- Hyperthreading.
nproccounts logical cores. Two threads on one physical core don't deliver 2× throughput; you notice that above load ≈ 0.7 × physical cores.lscpu | grep -E 'Thread|Core'. - Containers.
/proc/loadavgin a container is the load of the whole host. Look atcpu.statandcpu.pressurein the container's cgroup (/sys/fs/cgroup/<path>/cpu.pressure). - NFS hangs. An unreachable NFS server puts every process touching the path into
Dstate. Load rises to hundreds while the CPU is idle.io fullin PSI sees it immediately; the load alert only says "something". - Alerting on the 1-minute value. Every
logrotatewith gzip produces a spike. Use 5 or 15 minutes, or PSIavg300. - Fixed thresholds on varying hardware. "load > 4" means something different on your 2-core dev VM than on the 48-core database. Normalise per core, or use PSI, which is already a percentage.
What you still don't have
- History. "Since when has I/O pressure been at 8%?" needs a time series;
sar -qgives load, but no PSI. - Normal per hour. On Tuesday night load 6 is normal (backup); on Tuesday afternoon it isn't. A fixed threshold doesn't know the difference.
- The link to the cause. Pressure high at 03:00 and the backup job that started at 02:55: two logs.
How monsys does it
The monsys agent measures load, r/b from vmstat, steal and the three PSI values every 15 seconds, and the hub builds an hour-of-day baseline per host: an anomaly_load_avg rule fires on deviation from what this host normally does at this hour, not on an absolute number. The alert shows the top processes and the I/O statistics of the same moment, so the table above is already filled in before you log in.
FAQ
What is a good load average?
Per core (load divided by nproc) below 0.7 on the 15-minute figure: then there's headroom. Between 0.7 and 1 the machine is full but without a queue. Above 1 tasks wait structurally — or there's an I/O problem, which you check with vmstat (columns b, wa) or /proc/pressure/io.
Why is my load high while the CPU is idle?
Because Linux counts processes in uninterruptible sleep (D state, waiting on disk, NFS or swap). Look at ps -eo state,comm | grep ^D and /proc/pressure/io. More CPU doesn't help here; faster storage or a fix for the hanging mount does.
Is PSI better than load average?
Yes, for alerting: it separates CPU, memory and I/O, it's a percentage (no normalisation needed) and it measures wait time rather than task count. Load average remains useful as one quick number, but build your alerts on /proc/pressure.
Written by the monsys team — sysadmins who do this every day.
Done it by hand? Let monsys keep it running.
Everything in this guide runs in monsys as a continuous check, with history, alerts and audit evidence. 5 servers free, EU-hosted in Belgium, installed in 60 seconds.