LinuxLXVI · Capacity Planning for ClustersMeasurement
Utilisation is not saturation - what the queue tells you that the percentage cannot
What you'll learn
- Distinguish utilisation, saturation and errors as three separate measurements
- Predict the latency penalty of a given utilisation using the queueing multiplier
- Measure saturation with run queue length, await times and pressure stall information
- Explain why disk %util is misleading on multi-queue NVMe devices
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-11
A capacity ceiling expressed as a percentage carries a hidden assumption: that the resource performs the same at 80% as it did at 40%, only with less room left. That assumption is false for every resource that has a queue in front of it, which is all of them.
This is why “we still have 20% headroom” and “the service is slow” are routinely true at the same time, and why the engineer holding the dashboard and the engineer holding the pager cannot agree.
Three measurements, not one
The USE method names them, and the value of the naming is that it stops one number being asked to do three jobs.
- Utilisation: the fraction of time the resource was busy. A percentage. Bounded at 100%.
- Saturation: how much work is waiting because the resource was busy. A queue length or a wait time. Unbounded.
- Errors: work the resource refused or dropped. A count.
Utilisation tells you how full the road is. Saturation tells you how long the traffic jam is. Only the second one correlates with what a user experiences, and only the second one keeps rising after utilisation has flattened at 100%.
Two hosts both reporting 100% CPU are not in the same state if one has a run queue of 2 and the other a run queue of 60. The percentage cannot tell them apart. The queue can.
The queueing multiplier
For a single server with random arrivals, the classic result is that the average time in the system - queueing plus service - grows as:
response time = service time / (1 - u)
where u is utilisation expressed as a fraction. The multiplier
is worth memorising, because it explains almost every “it was
fine yesterday” incident:
| Utilisation | Multiplier 1 / (1 - u) | 10 ms of work takes |
|---|---|---|
| 50% | 2.0 | 20 ms |
| 70% | 3.3 | 33 ms |
| 80% | 5.0 | 50 ms |
| 90% | 10.0 | 100 ms |
| 95% | 20.0 | 200 ms |
| 99% | 100.0 | 1000 ms |
Going from 50% to 70% costs you 13 ms. Going from 90% to 95% costs you 100 ms. The same twenty-point move costs eight times as much at the top of the range, which is the whole reason capacity ceilings are set well below 100%.
Measuring CPU saturation
Utilisation comes from %idle. Saturation comes from the run
queue: the count of threads that are runnable but not running.
$ nproc; sar -q LOAD 1 312
03:41:02 PM runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15 blocked
03:41:03 PM 31 902 18.44 9.71 4.02 0
03:41:04 PM 27 903 18.51 9.88 4.09 0
03:41:05 PM 34 901 18.66 10.02 4.17 0
Average: 31 902 18.54 9.87 4.09 0Illustrative output
The useful derived figure is runq-sz / nproc. Below 1 there is
no meaningful contention. At 3, wall-clock time for CPU-bound
work is roughly three times its CPU time, and no amount of
profiling the application will change that.
The blocked column is the second saturation signal: threads in
uninterruptible sleep, which almost always means waiting on I/O.
A high blocked count with a low runq-sz says the CPU is not
your problem at all.
Measuring disk saturation
%util is the field everyone reads and the one that lies
hardest on modern hardware.
$ iostat -xz 1 3 | tail -8Device r/s w/s rkB/s wkB/s r_await w_await aqu-sz %util
nvme0n1 41.0 6820.0 1312.0 872960.0 0.28 6.91 47.62 99.90
sda 3.0 91.0 48.0 1108.0 1.11 88.40 8.13 99.20Illustrative output
%util is the percentage of wall-clock time during which the
device had at least one request in flight. For a single-queue
rotational disk that was a good proxy for saturation, because
one request in flight meant the one mechanism was occupied.
An NVMe device has many hardware queues and serves requests
concurrently. It reaches 100% %util with one request
outstanding while running at a fraction of its capability, and
it stays at 100% all the way to genuine saturation. On such a
device %util is a busy indicator with no capacity meaning
whatsoever.
Pressure stall information
Run queue and await are per-resource proxies. Since kernel 4.20,
/proc/pressure reports the thing you actually wanted: the
percentage of wall-clock time during which work was delayed by
a shortage of CPU, memory or I/O.
$ cat /proc/pressure/cpu /proc/pressure/io /proc/pressure/memorysome avg10=0.00 avg60=0.01 avg300=0.00 total=811140006
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
some avg10=14.72 avg60=11.08 avg300=7.41 total=94822013301
full avg10=9.30 avg60=7.02 avg300=4.66 total=52117884204
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0Illustrative output
Read it as follows.
someabove a few percent means the resource is already costing you latency, whatever the utilisation percentage says.fullabove zero for CPU or I/O means wall-clock time in which nothing progressed. Any sustained non-zerofullis a capacity finding, not a tuning opportunity.- The
avg10,avg60andavg300windows let you separate a momentary burst from a sustained condition without storing high-resolution samples.
sar -q PSI records the same counters into the sysstat history,
so you can go back and ask what the pressure was during last
Tuesday’s incident rather than only what it is now.
What this changes about the ceiling
The survivable ceiling from the N+1 arithmetic - N x u / (N-1)
reaching 100% - assumes a node can serve at 100% utilisation.
The queueing multiplier says it cannot serve acceptably there.
Take three nodes at the ceiling of 66.7%. Lose one, and the two survivors sit at 100% by the arithmetic. By the multiplier, a request that took 33 ms at 66.7% does not take 50 ms at 100% - it takes as long as the queue is deep, and the queue grows for as long as arrivals exceed service. The failover does not degrade the service; it stops it.
That is why every capacity lesson in this course says the same thing in a different way: the ceiling is where failure becomes outage, and the operating target sits well below it. Saturation metrics are how you find out where “well below” actually is for your workload, rather than guessing at a percentage.
- Take the utilisation baseline: median, p95 and max per role
- Take the saturation baseline alongside it: runq-sz/nproc, await, PSI some and full
- Find the utilisation at which your saturation metric starts to climb - that is your knee, measured rather than assumed
- Set the operating target below the knee, and the alert threshold on the saturation metric rather than on utilisation
- Re-measure the knee after any change to hardware, kernel or workload shape
Knowledge check
Knowledge check · 6 questions
Q1. Two hosts both report 100% CPU utilisation. One has a run queue of 2, the other 60. What does the utilisation figure tell you about the difference?
Q2. Using the queueing multiplier 1/(1-u), what does moving from 90% to 95% utilisation cost a 10 ms unit of work, compared with moving from 50% to 70%?
Q3. An NVMe device shows 99.9% util with w_await of 6.9 ms, and a SATA disk shows 99.2% util with w_await of 88 ms. What should you conclude?
Q4. In /proc/pressure/io, what does a sustained non-zero value in the full line mean?
Q5. Which of these are saturation measurements rather than utilisation measurements? Select all that apply.
Q6. A multi-queue NVMe device can report 100% util while serving requests at a small fraction of its capability.
Passing score: 75%. Answers are checked in this browser.