CoursesAdvanced Linux internals & toolingThe USE method & finding bottlenecks

The USE method & finding bottlenecks

A repeatable way to locate the constraint.

Advanced14 min · lesson 1 of 17

Most performance work goes sideways the same way. Something is slow, so you log in, and within a minute you are already tuning: bumping a thread pool, adding memory, flipping a low-level system setting you read about once. You are guessing. Sometimes the guess lands and you look like a hero. More often it costs you an afternoon, because you spent it tuning a resource that was never the problem. The USE method is how you stop guessing and find the actual constraint first.

Three Questions, Every Resource

Start at a supermarket checkout, because the whole method fits there. Utilization is how many lanes are open and actively ringing up customers. Saturation is the line waiting behind them, the shoppers who want a lane and cannot have one yet. Errors are the transactions that fail: a declined card, a register that crashes, an item that will not scan. Ask those three questions about a checkout and you have the USE method whole: Utilization, Saturation, and Errors. You ask them about every physical resource the machine leans on, the CPU (central processing unit, the chip that actually runs your code), memory, disk, and network. And here is the part people miss. A store can be fully utilized, every lane busy, and still be perfectly healthy, as long as nobody is standing in line.

Now the same idea in kernel terms, where the kernel is the core part of the operating system that talks straight to the hardware. Utilization is the fraction of an interval a resource was busy doing work; a disk that spent 90% of the last second servicing requests is 90% utilized. Saturation is the work that piled up because the resource could not keep pace: the run queue of processes wanting a CPU, the depth of the disk's input/output (IO) request queue, the memory the kernel had to push out to swap (disk space it uses as overflow when physical memory fills). Errors are countable failures: a failed disk write, a dropped packet, a process the kernel killed for running the box out of memory. Of the three, saturation tracks pain most closely. Full utilization with no queue is a resource working hard and coping fine. A queue means something is waiting, and waiting is what your users actually feel.

USE: ask all three of every resource
Utilization (how busy)
CPU % busy
mpstat -P ALL
Disk % util
iostat -xz
Memory used
free -m
Net throughput
sar -n DEV
Saturation (who is waiting)
Run queue r
vmstat
Swap si/so
vmstat
Disk queue aqu-sz
iostat
Stall time
/proc/pressure
Errors (what failed)
OOM kills
dmesg -T
IO errors
dmesg -T
Net drops
ip -s link
Memory errors
edac-util
Walk CPU, memory, disk, network. The resource that is saturated or erroring is your constraint.

The First Sixty Seconds

Before you reach for tracers and flame graphs (heavier tools that visualize exactly where time goes), spend one minute getting oriented. Brendan Gregg, the performance engineer who created the USE method and later wrote Netflix's sixty-second triage checklist, teaches a fixed opening sequence of cheap commands. Depth is not the goal here. Direction is. You want to know which resource deserves the next hour of your attention before you sink the hour.

~/secopslog — bash
$ uptime # load trend over 1/5/15 min: rising or falling?
15:42:31 up 12 days, 3:14, 2 users, load average: 8.10, 5.22, 3.15

Those three numbers are the load average over the last one, five, and fifteen minutes. Count them the way you would count people in the store: everyone who wants a register, both the shoppers already at one and the shoppers stuck in line behind them. Read left to right for the trend. 8.10, then 5.22, then 3.15 means the most recent minute is the busiest of the three, so load is building rather than draining. This is a four-core box, so a load of eight sounds alarming. Hold that thought. Load average is the single most misread number in Linux, and the next command tells you what it actually counts.

~/secopslog — bash
$ vmstat 1 5 # per-second flow of run queue, swap, IO wait
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 0 0 128980 22908 742880 0 0 12 46 110 190 12 4 82 2 0 1 3 0 128364 22916 743210 0 0 4 4096 520 1120 6 3 41 50 0 2 4 0 127110 22916 743480 0 0 0 5120 610 1240 5 4 30 61 0 1 4 0 126980 22916 743520 0 0 8 4864 590 1198 6 4 28 62 0 1 3 0 126840 22916 743560 0 0 0 4608 560 1155 5 3 33 59 0

Ignore the first row; it is an average since boot, not the live picture. Watch the rows underneath. Two columns under procs matter: r is the run queue, the count of tasks that want a CPU right now, and b is tasks blocked waiting on IO, mostly disk and network. Under cpu, wa is iowait: the share of time the processor sat idle with nothing to do but wait for storage. Here r stays at one or two while wa holds above fifty. So the CPU is not the bottleneck. It is standing around waiting for the disk to answer. si and so, swap in and swap out, are both zero, so memory is not thrashing either. The story already points hard at storage.

~/secopslog — bash
$ mpstat -P ALL 1 1 # is it one hot core, or all of them?
Linux 5.15.0-91-generic (web01) 07/17/2026 _x86_64_ (4 CPU) 15:42:33 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle 15:42:34 all 6.25 0.00 3.50 58.75 0.00 0.50 0.00 0.00 0.00 31.00 15:42:34 0 5.00 0.00 4.00 62.00 0.00 1.00 0.00 0.00 0.00 28.00 15:42:34 1 7.00 0.00 3.00 55.00 0.00 0.00 0.00 0.00 0.00 35.00 15:42:34 2 6.00 0.00 4.00 60.00 0.00 1.00 0.00 0.00 0.00 29.00 15:42:34 3 7.00 0.00 3.00 58.00 0.00 0.00 0.00 0.00 0.00 32.00

mpstat breaks the same picture out per core, and that matters because an average hides a single-threaded bottleneck. One core pinned at 100% while the other three sit idle averages out to a comfortable-looking 25%, and you would never feel the pain in the summary. Here every core shows the same high %iowait, which confirms the load is spread and storage-bound, not stuck in one thread. If instead you saw a single CPU at 100% %usr with the rest idle, you would go hunting for a process trapped on one thread.

~/secopslog — bash
$ dmesg -T | tail -4 # the E in USE: recent kernel errors
[Fri Jul 17 15:38:02 2026] sd 0:0:0:0: [sda] tag#12 FAILED Result: hostbyte=DID_ERROR driverbyte=DRIVER_OK cmd_age=0s [Fri Jul 17 15:40:19 2026] EXT4-fs warning (device sda1): ext4_end_bio:342: I/O error 10 writing to inode 26241 starting block 8402531 [Fri Jul 17 15:41:55 2026] Out of memory: Killed process 5123 (python3) total-vm:6142200kB, anon-rss:5980100kB, file-rss:0kB, shmem-rss:0kB, UID:998 pgtables:11804kB oom_score_adj:0 [Fri Jul 17 15:41:55 2026] oom_reaper: reaped process 5123 (python3), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

dmesg is the E in USE. The kernel logs its worst news here: storage failures, filesystem corruption, and the out-of-memory (OOM) killer choosing a process to sacrifice when memory runs dry. The -T flag prints human-readable timestamps instead of raw seconds since boot. One glance tells you whether you are chasing a tuning problem or a dying disk. A real IO error in this log rewrites the whole investigation, because no amount of tuning fixes hardware that is on its way out.

Load average is not CPU usage
Load average counts every task that is running OR waiting, and 'waiting' includes processes in uninterruptible sleep (state D), almost always blocked on disk IO. So a load of 8 on a box with idle CPUs usually means storage saturation, not a busy processor. A load above your core count is a reason to investigate, never automatic proof the CPU is maxed. Confirm with vmstat every time: is the run queue r high, or is it iowait wa? Chase the CPU while the disk is the real constraint and you will burn hours.

Saturation You Can Actually Measure

Utilization tells you a resource is busy. It does not tell you anyone is suffering for it. For that you want saturation, and modern Linux hands you a direct readout the old tools never had: Pressure Stall Information (PSI), living under /proc/pressure. Think of it as the store measuring how much of the last few minutes shoppers spent frozen in line instead of moving. PSI answers one blunt question per resource. What share of recent wall-clock time did tasks spend stalled, unable to make progress because they were stuck waiting on this resource?

~/secopslog — bash
$ cat /proc/pressure/io
some avg10=57.14 avg60=42.88 avg300=19.03 total=98765432 full avg10=39.02 avg60=31.66 avg300=14.11 total=71234560

Two lines, two meanings. some is the percentage of time at least one task was stalled waiting on IO. full is the percentage of time every non-idle task was stalled at once, which means the machine did no useful work at all while it waited. The three numbers are averages over the last 10, 60, and 300 seconds; total is the raw count of stalled microseconds since boot. An io some of 57 says that for well over half of the last ten seconds, something was stuck on the disk. That is saturation stated as a plain percentage, and it drops onto the USE method cleanly. Matching files sit at /proc/pressure/cpu and /proc/pressure/memory. When you want to alert on 'is this resource actually hurting,' PSI usually beats utilization as a signal, because it measures the waiting, not the busyness.

Name The Process

USE finds the constrained resource. It does not hand you the culprit using it. Once the io pressure line is screaming, you switch questions, from which resource to who is holding it down. iostat gives you the per-device view.

~/secopslog — bash
$ iostat -xz 1 2 # -x extended stats, -z hide idle devices
Linux 5.15.0-91-generic (web01) 07/17/2026 _x86_64_ (4 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 6.25 0.00 4.00 58.75 0.00 31.00 Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util sda 2.00 32.00 0.00 0.00 1.50 16.00 210.00 8400.00 18.00 7.90 46.80 40.00 9.85 98.40

Most of those columns are noise during triage. Three earn your attention. %util is the share of the interval the device had at least one request in flight (utilization). aqu-sz is the average number of requests sitting in the queue (saturation). w_await is the average milliseconds a write waited, queue time included (latency, the number your users actually feel). Here sda sits at 98% util with a queue depth near ten and writes waiting almost 47 milliseconds each. The disk is pinned. Now go find the writer.

~/secopslog — bash
$ pidstat -d 1 # attribute disk IO to processes
Linux 5.15.0-91-generic (web01) 07/17/2026 _x86_64_ (4 CPU) 15:43:02 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 15:43:03 998 4021 0.00 8192.00 0.00 540 postgres 15:43:03 0 711 0.00 12.00 0.00 2 systemd-journal

pidstat -d attributes disk throughput to individual processes. kB_wr/s is kilobytes written per second, and iodelay is how long that process sat blocked on IO, counted in clock ticks. postgres is writing eight megabytes a second and soaking up nearly all the iodelay. There is your answer: the database is the load, not the operating system and not the kernel. iotop shows the same per-process disk view in a live, top-style display if you would rather watch it move. From here the fix is a database question, a missing index driving full-table scans, an over-aggressive checkpoint, a runaway backfill, and you can go have that conversation carrying evidence instead of a hunch.

What An Attacker Looks Like Through USE

The reason this sweep is worth burning into muscle memory is that it does not care whether the cause is a bad query or a bad actor. Both surface the same way, as a saturated resource. An unauthorized coin miner does not announce itself. It shows up as CPU utilization pinned near 100% with a run queue climbing behind it, and when you ask who, you find a process with an odd name burning every core.

~/secopslog — bash
$ ps -eo pid,%cpu,%mem,comm --sort=-%cpu | head -4
PID %CPU %MEM COMMAND 8842 391 1.2 kdevtmpfsi 4021 9 4.1 postgres 711 0 0.3 systemd-journal

%CPU here totals across cores, so 391 on a four-core box is a single process eating nearly the whole machine. The name kdevtmpfsi is a well-known miner that dresses itself up to look like a kernel thread; a genuine kernel worker never burns user-space CPU like this. The same sweep catches other attacks in their native USE signature. A memory-exhaustion bug or a fork bomb shows up first as saturation, swap in and out climbing in vmstat, and then as errors, OOM kills landing in dmesg. A volumetric network flood shows up as the interface saturating and receive drops climbing, which you read straight off the counters.

~/secopslog — bash
$ ip -s link show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff RX: bytes packets errors dropped missed mcast 198372645 1502398 0 18422 0 1204 TX: bytes packets errors dropped carrier collsns 142938471 1187233 0 0 0 0

On the RX (received) side, dropped climbing while errors stays at zero is the classic shape of a receive queue filling faster than the kernel can drain it, exactly what a flood or a misbehaving client produces. You did not set out to find an intrusion. You ran a performance sweep, and the saturated resource pointed right at it. That is the quiet security payoff of doing triage by method instead of by hunch.

%util lies on flash storage
On an old spinning disk that serves one request at a time, %util near 100 really does mean 'full.' On an SSD (solid-state drive) or NVMe (Non-Volatile Memory Express, a fast flash interface) that serves many requests in parallel, %util only means 'at least one request was in flight,' which can read 100% while the device still has plenty of headroom. On flash, trust aqu-sz (queue depth), await (latency), and raw throughput over %util. Alerting on %util alone will send you chasing phantom disk bottlenecks on hardware that was nowhere near its limit.
Quick check
01uptime on a four core box reads 8.10, 5.22, 3.15. You run vmstat 1 and r holds at 1 or 2, wa stays above 50, and si and so are both 0. Which resource is the constraint?
Incorrect — Load average counts tasks that are waiting as well as tasks that are running, so it can sit at eight while the chip idles. Here r of 1 or 2 says barely anyone is queued for a core.
Incorrect — Paging pressure would push si and so off zero and would usually leave OOM kill lines in dmesg -T. Both signals are absent, so memory is coping.
Incorrect — vmstat reports nothing about the NIC at all, and b covers disk waits as readily as network ones. Confirming a network limit needs ip -s link or sar -n DEV.
Correct — An iowait above 50 alongside a nearly empty run queue is the shape of tasks parked in uninterruptible sleep. They count toward load without ever touching a CPU.
02cat /proc/pressure/io prints a some line averaging 57 and a full line averaging 4. What are those two numbers telling you?
Incorrect — The 10, 60 and 300 second windows are the three columns on each line, so both some and full already report all three. The split between the lines is how many tasks were stalled, not over what span.
Correct — That is the distinction PSI draws: one or more tasks waiting versus all of them waiting at once, when the box gets nothing done. The full line is the more alarming of the two.
Incorrect — Device busyness and queue depth are what iostat gives you through %util and aqu-sz. PSI counts time that tasks lost to waiting, which is a different quantity.
Incorrect — Capacity questions belong to df, not to /proc/pressure. Every number under that path is a share of wall clock time spent stalled.
03ip -s link show eth0 reports the link UP with RX errors at 0, RX dropped climbing into the thousands, and TX dropped still at 0. What does that shape point to?
Incorrect — Damage on the wire lands in the errors counter, and errors here are flat at zero. The frames are arriving intact, they just have nowhere to go.
Incorrect — Outbound loss would raise TX dropped, which is still zero. Read the RX and TX columns separately or you will chase the wrong direction.
Correct — Clean frames landing with no room left to hold them is what a flood or a chatty client looks like through the counters, and it is saturation rather than an error.
Incorrect — A downed link would not report UP, and it would not be moving bytes at all. This interface is passing traffic, just not all of it.

One last habit. After you change something, run the exact same USE sweep again and watch the one number that was saturated. If the io pressure some line and iostat's aqu-sz both fall, you fixed the right thing. If they do not move, you did not, no matter how sensible the change looked on paper. The method that found the bottleneck is also the proof that your fix worked.

Try this

Work through “What An Attacker Looks Like Through USE” 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: load average is not CPU usage. 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