CoursesAdvanced Linux internals & toolingCPU & memory: load, vmstat, pressure

CPU & memory: load, vmstat, pressure

Read load average, PSI, and memory reality.

Advanced14 min · lesson 2 of 17

A load average of 6 sounds scary until you know how many people are working the counter. One number with no context barely tells you anything. That holds for almost every CPU (central processing unit, the chips that actually run your code) and memory figure on a Linux box: the raw number is meaningless until you know what it counts and what you are measuring it against. This lesson walks through the small set of numbers that tell you whether a machine is really in trouble, and the ones that look alarming while being completely normal. For a defender, that gap is the difference between chasing a ghost and catching a cryptominer.

What Load Average Actually Counts

Load average is the number you see in uptime, top, and every dashboard, and it is the one people get wrong most often. A small coffee shop has a few baristas behind the counter. At any moment some customers are being served and others are stuck in line. Load average is roughly the size of that whole crowd, served plus waiting, smoothed out over time. The three figures are exponentially weighted moving averages over the last 1, 5, and 15 minutes, which is a formal way of saying recent seconds count for more than older ones. The comparison that matters is load against core count. A load of 6 on a 4-core box means more work wants to run than you have baristas, so a line forms. A load of 6 on a 64-core box is a slow afternoon.

Here is where even experienced people get caught. Linux does not only count tasks that want the CPU. It also counts tasks stuck in uninterruptible sleep, known as D state, which almost always means they are blocked waiting on storage or the network. So a machine can show a load of 8 while the CPU sits 90% idle, because the load is not coming from compute at all. It is coming from processes frozen while a slow disk catches up. When load is high, your first question is always the same: is this CPU demand, or is this IO (input/output, the work of reading and writing data to disk or network) waiting? Every tool below is there to answer it.

~/secopslog — bash
$ uptime cat /proc/loadavg nproc
14:32:07 up 9 days, 3:14, 2 users, load average: 6.12, 4.80, 3.05 6.12 4.80 3.05 2/1043 48123 4

The fourth field in /proc/loadavg, 2/1043, is the number of tasks running or waiting to run at this instant, over the total number of processes and threads that exist on the box. The last value, 48123, is the process ID (PID) of the most recently created process. With four cores and a one-minute load of 6.12, this machine is oversubscribed on paper. Whether that actually hurts depends on why, and vmstat is where you start to see it.

vmstat: The One-Second Dashboard

vmstat (virtual memory statistics) is your live feed. Give it an interval and a count, and every line after the first is a fresh snapshot of that interval. Treat the first data line as scenery: it is an average since boot, not the current moment, so skip it. Read the rest the way you read a car dashboard while driving, glancing at what is happening right now.

~/secopslog — bash
$ vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 6 1 0 210344 18232 3401220 0 0 12 88 2201 4410 71 9 12 8 0 5 0 0 208112 18232 3402140 0 0 0 120 3050 6120 82 11 6 1 0 7 2 0 205540 18240 3402990 0 0 4 240 2890 5730 78 13 5 4 0

The columns that earn their keep: r is the run queue, the number of threads that want the CPU this instant, read against your core count exactly like load. b is the tasks blocked in that uninterruptible D state. si and so are memory swapped in from disk and out to disk (steady nonzero here is the alarm we get to shortly). us, sy, id, and wa are the percentages of CPU time spent in user code, kernel code, idle, and waiting on IO. st is steal time, the slice of time a hypervisor took your virtual CPU away and handed it to someone else. On a cloud host, steady st points at a noisy neighbor or CPU throttling, and no amount of application tuning will fix it. For a defender, a box that suddenly pins us above 90 with a climbing run queue and nothing in your deploy history to explain it is worth a hard look.

Whose CPU Is It

Knowing the CPU is busy is not the same as knowing who is burning it. Picture a kitchen at full tilt. mpstat (multiprocessor statistics) tells you which burner is roaring: it breaks usage down per core, so you can see whether one core is pegged (a single-threaded hog) or all of them are hot (a parallel job or a genuinely busy server). pidstat (per-process statistics) tells you which cook is standing over that burner: it attributes CPU to individual processes, handing you a name and a PID. Run them together and you catch the classic intrusion tell, a process you do not recognize eating a whole core.

~/secopslog — bash
$ mpstat -P ALL 1 1
Linux 6.1.0-18-amd64 (web01) 07/17/2026 _x86_64_ (4 CPU) 14:35:01 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle 14:35:02 all 71.25 0.00 9.05 8.00 0.00 0.45 0.25 0.00 0.00 11.00 14:35:02 0 98.00 0.00 2.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 14:35:02 1 62.00 0.00 10.00 12.00 0.00 1.00 0.00 0.00 0.00 15.00 14:35:02 2 70.00 0.00 9.00 9.00 0.00 0.00 1.00 0.00 0.00 11.00 14:35:02 3 55.00 0.00 15.00 11.00 0.00 1.00 0.00 0.00 0.00 18.00

CPU 0 is pinned at 98% user time while the other three coast. That shape is a single-threaded process, and mpstat can see it but cannot name it. pidstat can.

~/secopslog — bash
$ pidstat 1 1
Linux 6.1.0-18-amd64 (web01) 07/17/2026 _x86_64_ (4 CPU) 14:36:11 UID PID %usr %system %guest %wait %CPU CPU Command 14:36:12 0 1123 96.00 3.00 0.00 0.00 99.00 0 xmrig 14:36:12 1000 2044 28.00 2.00 0.00 1.00 30.00 2 node 14:36:12 0 814 0.00 1.00 0.00 0.00 1.00 1 systemd-journal

There it is. xmrig, a well-known cryptocurrency miner, running as root and holding 99% of a core, while node, your actual web app, ticks along at a normal 30%. On a server whose job is serving web traffic, a miner sitting on a full core is an incident. You stop tuning and open a case. pidstat handed you the PID and the command name on a single line, which is exactly what you carry into that investigation.

Memory: Cache Is Not Used Up

Now memory, and the biggest misconception in Linux operations. A good cook keeps the ingredients they reach for out on the counter instead of running back to the pantry every thirty seconds. The counter looks full, but the instant they need the space, they sweep it clear. Linux treats your RAM (random-access memory, the fast working memory the CPU reads from directly) the same way. Any RAM a program is not holding, the kernel (the core of the operating system that talks to the hardware) fills with a cache of recently read files, because empty RAM helps no one. That cache is reclaimable the moment a program asks for memory. So the free column in free -h can look terrifyingly small on a machine that is perfectly healthy.

~/secopslog — bash
$ free -h grep MemAvailable /proc/meminfo
total used free shared buff/cache available Mem: 15Gi 6.2Gi 0.4Gi 88Mi 8.4Gi 8.1Gi Swap: 2.0Gi 0B 2.0Gi MemAvailable: 8503124 kB

Look at the numbers. free reads 0.4Gi, which would send you into a panic if you stopped there. But available reads 8.1Gi, and available is the honest one: it estimates how much memory a new program could grab right now, counting all that reclaimable cache. The kernel publishes the same figure as MemAvailable in /proc/meminfo. Judge memory health by available and by swap activity, never by free alone. If available is comfortable, the box has memory to spare no matter how alarming free looks.

Pressure Stall Information: Measuring the Wait

Utilization has a blind spot. A resource can sit at 100% busy while nobody actually waits on it, and a resource can cause real pain long before it looks full. A kitchen running flat out is fine as long as every order still goes out on time. The number you actually care about is how long orders sat waiting. That is what Pressure Stall Information (PSI, a kernel feature that measures time lost to resource contention) gives you, across three files under /proc/pressure.

~/secopslog — bash
$ cat /proc/pressure/cpu cat /proc/pressure/io cat /proc/pressure/memory
some avg10=12.44 avg60=8.90 avg300=5.10 total=48213902 some avg10=42.10 avg60=28.33 avg300=15.72 total=88123401 full avg10=31.05 avg60=19.40 avg300=10.11 total=61203991 some avg10=0.00 avg60=0.00 avg300=0.00 total=120411 full avg10=0.00 avg60=0.00 avg300=0.00 total=98330

Each line reports the percentage of time tasks were stalled waiting on that resource, averaged over 10, 60, and 300 seconds, followed by a running total in microseconds since boot. The some line means at least one task was stalled. The full line means every task that could have run was stalled at the same moment, so nothing was getting done, which is the more serious of the two. CPU reports only some at the system level, because a whole machine does not fall into a full CPU stall the way it can for memory or IO. Here, io some sits at 42% over the last 10 seconds: for nearly half that window, something was stuck waiting on the disk. That is a real bottleneck, and it explains a high load average sitting next to idle CPUs far better than any utilization figure could.

PSI also works per cgroup (control group, the kernel feature that fences a set of processes together and caps what they can use), which is how containers and systemd (the program that starts and supervises services on a modern Linux) units get their own pressure readings. Point it at one service and you see exactly what that service is waiting on.

~/secopslog — bash
$ cat /sys/fs/cgroup/system.slice/nginx.service/io.pressure
some avg10=3.21 avg60=1.05 avg300=0.44 total=1820394 full avg10=2.90 avg60=0.98 avg300=0.40 total=1710233

When Memory Runs Out

When available finally runs dry, two things happen, in order. First the kernel starts swapping: copying pages of memory out to disk to free up room. You see it as steady nonzero si and so in vmstat, with swpd climbing. A little swap sitting idle is harmless. Continuous swapping is the cliff, because disk is thousands of times slower than RAM, and a box that leans on swap under load thrashes and slows to a crawl. Size the RAM, or cap the workload, so the machine never depends on swap to function. If memory keeps shrinking past that point, the kernel calls in the OOM (out of memory) killer. Think of it as a bouncer in a dangerously overpacked room: it picks one person by a score and throws them out so everyone else can keep breathing. It chooses a process by its oom_score and terminates it to save the system. The evidence lands in the kernel log.

~/secopslog — bash
$ sudo dmesg -T | grep -i 'killed process'
[Fri Jul 17 14:20:11 2026] Out of memory: Killed process 4823 (python3) total-vm:8123400kB, anon-rss:7810244kB, file-rss:132kB, shmem-rss:0kB, UID:1000 pgtables:15912kB oom_score_adj:0

That one line is the whole story of a kill: which process (python3, PID 4823), how much it was holding (anon-rss of roughly 7.4Gi of anonymous memory, meaning heap and stack, the kind not backed by any file), and its oom_score_adj, the dial that biases the killer toward or away from a given process. If a service keeps getting OOM-killed, this log is your proof that memory ran out, rather than your code crashing on its own.

Modern Ubuntu and Fedora ship systemd-oomd, a userspace daemon that watches PSI and steps in before the kernel's hard OOM killer ever fires. Rather than waiting for memory to hit zero, it kills a whole cgroup once memory pressure or swap usage crosses a threshold you configure. That is usually what you want, but it means a process can be terminated while free memory still looks fine, so read its log before you blame your application.

~/secopslog — bash
$ journalctl -u systemd-oomd -b --no-pager | tail -n 1
Jul 17 14:19:58 web01 systemd-oomd[812]: Killed /user.slice/user-1000.slice/[email protected]/app.slice/build.service due to memory pressure for /user.slice/user-1000.slice/[email protected] being 62.14% > 50.00% for > 20s with reclaim activity
systemd-oomd Can Kill While RAM Looks Fine
On Ubuntu 22.04 and similar, systemd-oomd makes its decision on PSI memory pressure and swap usage, not on free bytes hitting zero. A process can be killed while free -h still shows plenty of headroom, because pressure crossed a configured limit. If a service vanishes with nothing in its own logs to explain it, run journalctl -u systemd-oomd before you assume a bug in your code.
High Load: Where Is the Real Contention
Load average is high. Where is the contention?
r > cores, us/sy high
CPU saturation
Real compute demand. Find the process with pidstat and mpstat.
wa high, io.pressure some high
Disk or storage bound
Tasks blocked in D state; load counts them. Fix storage, not CPU.
free low, available high
Healthy cache
Not a memory problem. buff/cache is reclaimable on demand.
si/so steady, oomd fires
Memory exhaustion
The swap cliff. Size RAM or cap the workload.
Quick check
01A 4-core box reports a one minute load average of 8.0 while top shows the CPUs sitting 85% idle. Which reading fits both numbers?
Incorrect — Load is a smoothed average, so it does lag a little, but it never tracks runnable tasks alone; blocked ones sit in the same total.
Correct — A task waiting on a slow disk is counted exactly like one waiting for a core, which is how 8.0 sits beside idle CPUs.
Incorrect — If eight threads truly wanted a core, none of the four would be idle. The 85% is what rules this explanation out.
Incorrect — Swap is one way to park a task in D state, but any slow disk or network does it too, and you have no si or so evidence yet.
02vmstat on a cloud VM shows st holding steady at 12 after you have already tuned the application hard. What is that column reporting?
Correct — Steal is accounted for outside your guest, so the answer is a different host or instance size rather than more tuning.
Incorrect — Work the kernel does on your behalf is billed to sy, and code changes can shrink that. Steal is time you never received.
Incorrect — A priority fight shows up as one process holding more us time than you expect. Steal is a virtualization figure instead.
Incorrect — Waiting on swapped pages appears as wa with si and so moving, all of it measured inside your guest rather than in st.
03A service on Ubuntu 22.04 vanishes under load, its own logs stop mid-run, and free -h still shows several gigabytes available. Where do you look first?
Incorrect — free does hide what a single unit is holding, so the instinct is fair, but read the killer logs first; they name the victim.
Incorrect — The kernel killer waits until memory truly runs out, and gigabytes still available says it never got its turn.
Correct — It decides on PSI readings rather than free bytes, so a unit can die while the machine as a whole looks comfortable.
Incorrect — A silent stop with memory to spare is the shape of an outside kill. Rule that out before you go hunting for a bug.

Wire the pressure files into your monitoring, not the free number. An alert on io.pressure some crossing, say, 40% for a minute tells you a host is genuinely hurting, cleanly, with almost no false alarms. Then when load spikes, you already know whether to reach for pidstat and hunt a process or go check the disk. The metric that fires is the one that points you straight at the cause.

Try this

Work through “When Memory Runs Out” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: systemd-oomd Can Kill While RAM Looks Fine. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related