Linux Lesson 24 of 47

Performance Analysis & Tuning: CPU, Memory, Disk I/O, the USE Method, tuned & sysctl

There is a moment every Linux operator meets: an alert fires, someone says “the server is slow,” and a dozen dashboards all look a little bit red. The amateur move is to start guessing — restart the service, add more RAM, blame the network, reboot and hope. The professional move is to measure, in order, until the numbers name exactly one bottleneck, change exactly one thing, and then re-measure to prove it moved. That discipline is the whole of performance work, and it is entirely learnable.

This lesson teaches the discipline first and the tools second, because tools without a method just give you more numbers to panic about. We start with the USE method — for every resource, ask three questions: how utilized is it, how saturated (queued) is it, and is it throwing errors? Then we walk CPU, memory, disk, and network with the real commands that answer those questions, decode every cryptic column, and finish by fixing what we find with tuned, sysctl, and ulimit. Everything here runs on a throwaway VM, a cloud instance, WSL2, or a container — type the commands as you read.

Why this matters

“Slow” is not a diagnosis; it is a symptom, and it can come from four completely different places. A box can be slow because the CPU is pegged (real work, or a runaway loop), because memory ran out and the machine is thrashing swap or the OOM killer is culling processes, because the disk cannot keep up and every process is stuck in I/O wait, or because the network is dropping and retransmitting packets. The tools you reach for, and the fix you apply, are different in each case. Reach for the wrong one and you spend an hour tuning the CPU while the real problem is a disk queue twelve requests deep.

Beginners get three things wrong here, over and over. They misread load average as “CPU percent” (it is neither a percent nor CPU-only). They panic at free showing almost no free memory, not knowing the kernel deliberately fills RAM with reclaimable cache (“Linux ate my RAM” — it didn’t). And they guess at fixes — bumping vm.swappiness they read about on a forum — without first proving the box is even memory-bound. This lesson kills all three mistakes from first principles.

The mental model to hold for the whole lesson: a computer is four resources — CPU, memory, disk, network — and every performance problem is one of them being either fully used, backed up with a queue, or erroring. Your job is to find which one, with evidence, before you touch a single knob. Get that habit and you will out-diagnose people with ten more years of experience who are still guessing.

The method first: the USE method and the toolmap

Before any command, learn the framework that tells you which command to run. The USE method, popularised by performance engineer Brendan Gregg, says: for every resource, check three things.

Term Question it answers What “bad” looks like
U — Utilization What fraction of the time (or capacity) is the resource busy? CPU near 100% busy; memory fully allocated; disk %util high; link near line-rate
S — Saturation How much extra work is queued and waiting because the resource can’t keep up? A long run queue; swap in/out activity; disk aqu-sz and await climbing; NIC drops/backlog
E — Errors Is the resource returning hard failures? ECC/MCE events; OOM kills; disk I/O errors (EIO) in dmesg; TCP retransmits, NIC rx/tx errors

The power of USE is that utilization alone lies. A disk at 100% %util on an NVMe SSD may be perfectly happy (it serves many requests in parallel), while a disk at 60% %util with a queue depth of 12 is the real bottleneck. You only see that by also checking saturation. Three questions, four resources — twelve cells — is the fastest known path from “it’s slow” to “here is the bottleneck.”

Now map each resource to the tools that answer those three questions. This is the toolmap you will use for the rest of your career:

Resource Observe utilization Observe saturation Observe errors
CPU top, htop, mpstat -P ALL, vmstat (us+sy) vmstat r column, uptime load avg, pidstat dmesg (MCE), perf
Memory free -h, /proc/meminfo, vmstat (free/cache) vmstat si/so, swap used, sar -W dmesg (OOM killer), /proc/vmstat
Disk I/O iostat -xz 1 %util, iotop iostat await/aqu-sz, pidstat -d dmesg (I/O errors), smartctl
Network sar -n DEV, ip -s link, ethtool ss -s, nstat, sar -n EDEV drops ss -ti retrans, ethtool -S, nstat

The four resources each get a family of “observe” tools that feed a single “tune” action — a tuned profile, a sysctl, or a ulimit. That is the shape of the entire workflow, and it is worth seeing as one picture before we dive in:

Left-to-right performance toolmap showing the USE-method diagnostic workflow: a SYMPTOM zone (the server-is-slow page firing, with load average from uptime as the first read) flows into two OBSERVE zones — CPU and memory measured with top, mpstat, pidstat, free and vmstat si/so, then disk I/O and network measured with iostat, iotop, ss and sar — which feed a USE VERDICT zone that classifies the problem as Utilization, Saturation, or Errors, which in turn drives a TUNE AND VERIFY zone where you change exactly one thing (a tuned profile, a sysctl, or a ulimit) and re-measure to prove it moved; numbered badges mark that load average is run-queue length not CPU percent, that free RAM under buff/cache is reclaimable cache not a leak, that USE means checking all three signals per resource, that the OOM killer picks a victim by oom_score, that tuned swaps dozens of kernel knobs at once, and that sysctl -w is temporary while an /etc/sysctl.d file persists

Read it left to right: the page fires, you observe every resource, the USE questions turn raw counters into a verdict naming one bottleneck, and only then do you tune. The badges call out the six ideas people get wrong — start with the first one, because it is the number everyone reads first and almost everyone misreads.

Brendan Gregg also splits observability tools by how they get their data, which tells you how much overhead and detail to expect:

Tool type Examples What it gives you Cost
Fixed counters vmstat, free, iostat, sar, /proc Kernel-maintained running totals, summarised per interval Nearly free
Profiling (sampling) perf top, perf record Where CPU time is spent, by function/stack, from periodic samples Low, tunable
Tracing ftrace, perf trace, eBPF/bpftrace Every event (syscall, block I/O, scheduler) with full context Higher, targeted
Monitoring sar/sysstat, Prometheus/node_exporter The history — what the box looked like at 03:00 last night Storage + agent

You start at the top (cheap counters) and only descend to tracing when the counters point somewhere but can’t say why. For the deepest layer — per-event kernel tracing with eBPF — see Observability with eBPF, bpftrace, perf & ftrace; this lesson deliberately stays in the counters-and-profiling layers you reach for first.

Most of these tools live in one package. Install it before you go further:

# Debian / Ubuntu — sysstat gives iostat, mpstat, pidstat, sar; plus the interactive extras
sudo apt update && sudo apt install -y sysstat htop iotop procps

# RHEL / Fedora / Rocky / Alma
sudo dnf install -y sysstat htop iotop procps-ng

procps/procps-ng provides top, vmstat, free, uptime and is usually already present. sysstat is the one you almost always have to add.

Reading load average correctly

Everyone’s first performance metric is load average, and almost everyone reads it wrong. It appears in uptime, top, w, and /proc/loadavg:

# The three load-average numbers: 1-minute, 5-minute, 15-minute exponentially-weighted averages
uptime
#  14:23:01 up 12 days,  3:44,  2 users,  load average: 7.81, 4.20, 2.15

Load average is not a percentage and it is not CPU-only. On Linux it is the average number of processes (technically tasks) that are either R — running/runnable (on a CPU or waiting for one) or D — in uninterruptible sleep (blocked in the kernel, almost always on disk or NFS I/O). That second half is the part beginners miss: a machine with an idle CPU but a saturated disk can show a load of 20, because twenty tasks are all stuck in D state waiting for the disk. Load average measures demand for the system, not CPU busyness.

The only way to read the raw number is against your core count. A load of 8 means “on average, 8 tasks wanted to run at once.”

Load average vs. cores Interpretation
Load < cores Spare capacity — tasks rarely wait
Load cores Fully utilized, no queue — the healthy busy point
Load > cores Saturated — more demand than CPUs; tasks are queuing
Load cores while CPU idle Demand is not CPU — likely D-state, i.e. a disk/IO bottleneck
# How many cores do I have to compare against?
nproc
# 4

# Or the detailed topology
lscpu | grep -E '^CPU\(s\)|Core|Socket|Thread'
# CPU(s):                4
# Thread(s) per core:    2
# Core(s) per socket:    2
# Socket(s):             1

So load average: 7.81, 4.20, 2.15 on a 4-core box means: right now (1-min) demand is ~2x capacity and rising (1-min > 5-min > 15-min — the spike is recent and getting worse). If the order were reversed (2.15, 4.20, 7.81) the storm would be passing. Reading the trend across the three numbers is half the value.

The three columns decoded:

Field Meaning
1st number Load averaged over the last 1 minute — the “right now”
2nd number Over the last 5 minutes — the recent trend
3rd number Over the last 15 minutes — the baseline
Rising (1 > 5 > 15) Load is increasing — the incident is active
Falling (1 < 5 < 15) Load is decreasing — the incident is receding

To confirm whether a high load is CPU demand or D-state I/O wait, count the states directly — this single command tells you which of the two halves of load average is doing the damage:

# Count runnable (R) vs uninterruptible (D) tasks right now
ps -eo state | grep -c '^R'   # runnable / running  -> CPU demand
ps -eo state | grep -c '^D'   # uninterruptible      -> stuck on I/O

# See exactly which tasks are stuck in D (blocked on disk/NFS)
ps -eo pid,state,cmd | awk '$2 ~ /D/'

A load of 20 that is mostly R is a CPU problem; a load of 20 that is mostly D is a disk problem wearing a CPU costume. Process states (R, S, D, T, Z) are covered in depth in Processes & Jobs: ps, top, signals & kill; here they are the fork in the diagnostic road.

CPU: is it busy, and with what?

Load told you demand exists. Now find out whether the CPU itself is the bottleneck, and if so, doing what — user code, kernel/system work, or waiting on I/O.

top and htop — the live dashboard

top is on every box. The header line and the %Cpu(s) line are the parts that matter:

top
top - 14:23:01 up 12 days,  3:44,  2 users,  load average: 7.81, 4.20, 2.15
Tasks: 214 total,   6 running, 208 sleeping,   0 stopped,   0 zombie
%Cpu(s): 71.3 us,  9.8 sy,  0.0 ni, 12.1 id,  6.4 wa,  0.0 hi,  0.4 si,  0.0 st
MiB Mem :   7960.0 total,    412.3 free,   5820.1 used,   1727.6 buff/cache
MiB Swap:   2048.0 total,    108.0 free,   1940.0 used,    980.4 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   3312 app       20   0 4820100 3.1g   12m R  63.1  40.0  88:14.02 java
   3410 app       20   0  912000 210m   8m R  22.7   2.6  10:02.11 python3

The %Cpu(s) line is the single most information-dense row in Linux performance. Every field is a percentage of total CPU time over the refresh interval, and they sum to ~100%:

Field Name What it means Watch for
us user Time running user-space code (your apps) High = app doing real (or runaway) work
sy system Time in the kernel on behalf of processes (syscalls) High = syscall-heavy: lots of I/O, context switches, network
ni nice User time of re-niced (low-priority) processes Usually 0 unless you use nice
id idle CPU doing nothing Low id + high us/sy = CPU-bound
wa iowait Idle because it’s waiting for disk/IO to return High = likely I/O-bound, not CPU-bound
hi hardirq Servicing hardware interrupts High = NIC/storage IRQ storm
si softirq Servicing software interrupts (network stack, timers) High si = heavy network packet processing
st steal Time the hypervisor gave to another VM On cloud VMs, high st = noisy neighbour / oversubscribed host

Two of these are the ones people skip and shouldn’t: wa tells you the CPU is idle only because it’s blocked on I/O — high wa redirects you to the disk section. st (steal) only appears on virtualized guests and is your evidence that the host is oversubscribed — no amount of guest tuning fixes it; you resize or move the instance.

htop (sudo apt install htop / sudo dnf install htop) is the friendlier, colour version: per-core meters, tree view (F5), searchable, and you can send signals without leaving it. Same data, nicer glass.

mpstat — is the load spread or stuck on one core?

top’s %Cpu(s) line is an average across all cores. That average hides the classic single-threaded bottleneck: one core pinned at 100% while seven sit idle shows as “12% CPU” and looks fine. mpstat -P ALL breaks it out per core:

# -P ALL = every CPU; the trailing 1 = refresh every 1 second
mpstat -P ALL 1
02:31:07 PM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %idle
02:31:08 PM  all   68.42    0.00    8.31    5.20    0.00    0.51    0.00   17.56
02:31:08 PM    0   99.01    0.00    0.99    0.00    0.00    0.00    0.00    0.00
02:31:08 PM    1    5.10    0.00    2.04    3.06    0.00    0.00    0.00   89.80
02:31:08 PM    2    4.08    0.00    1.02    2.04    0.00    0.00    0.00   92.86
02:31:08 PM    3    6.19    0.00    3.09    4.12    0.00    1.03    0.00   85.57

CPU 0 is at 100% (%idle 0.00) while 1–3 are nearly idle. That is a single-threaded workload that cannot use more cores — throwing more vCPUs at it will do nothing. This one view saves countless wrong “just scale up” decisions.

pidstat — which process, over time

top sorts by instantaneous CPU; pidstat gives you a timestamped log per process, which is far better for catching intermittent spikes and for after-the-fact evidence:

# Per-process CPU every 2 seconds; add a count to stop (5 samples)
pidstat 2 5
# 02:35:10 PM   UID    PID    %usr %system  %guest   %wait    %CPU   CPU  Command
# 02:35:12 PM  1001   3312   58.00    6.50    0.00    2.00   64.50     0  java
# 02:35:12 PM  1001   3410   20.00    3.00    0.00    1.50   23.00     3  python3
pidstat option Shows
pidstat 1 Per-process CPU each second
pidstat -d 1 Per-process disk I/O (kB read/write per sec)
pidstat -r 1 Per-process memory (faults, RSS)
pidstat -w 1 Per-process context switches (voluntary/involuntary)
pidstat -t 1 Break down by thread, not just process
pidstat -p PID 1 Focus on one PID

%wait in that output is CPU run-queue wait (the task was ready but no core was free) — a direct saturation signal at the process level.

vmstat — the whole machine in one line per second

If you learn one command from this lesson, make it vmstat. One tidy line per second summarising CPU, memory, swap, I/O, and system activity together — the fastest “what kind of slow is this” read there is:

# Refresh every 1s. The FIRST line is an average since boot — ignore it; read from line 2.
vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 8  1 1986048 421900  18240 1707800    0    4     6    28  980 4120 71 10 12  6  1
 9  0 1986048 418020  18240 1709220  120   80  4096  8192 5200 9800 68 12  4 15  1
 7  2 1985024 420110  18244 1708900   40   16   512   256 1100 4500 74  9 15  1  1

Decode every column — this table is worth memorising:

Group Col Meaning What it tells you
procs r Tasks runnable (running or waiting for CPU) r > cores, sustained = CPU saturated
procs b Tasks in uninterruptible sleep (blocked on I/O) b > 0, sustained = I/O bottleneck
memory swpd Virtual memory swapped out (KB) Large + growing = under memory pressure
memory free Idle memory (KB) Low is normal (cache); not a problem by itself
memory buff / cache Buffers + page cache The reclaimable cache — see memory section
swap si Memory swapped IN from disk (KB/s) Non-zero = active thrashing, a red flag
swap so Memory swapped OUT to disk (KB/s) Sustained > 0 = memory pressure
io bi Blocks received from disk (KB/s in) Read throughput
io bo Blocks sent to disk (KB/s out) Write throughput
system in Interrupts per second Very high = IRQ storm (NIC/timer)
system cs Context switches per second Very high = lock contention / too many threads
cpu us sy id wa st Same as top: user, system, idle, iowait, steal The CPU-time split

Now read the three lines above like a doctor: r is 7–9 on a 4-core box (CPU saturated), us is ~70% (user code, not kernel), si/so spiked to 120/80 on line 2 (a moment of swap thrash), and wa jumped to 15 when bo hit 8192 (a burst of writes stalling the CPU). In three seconds vmstat told you: primarily CPU-bound in user space, with memory pressure starting to bite. That is the read the whole lesson is teaching.

Context switches, the run queue, and CPU-bound vs I/O-bound

Two vmstat columns deserve their own paragraph. cs (context switches) counts how often the kernel swapped one task off a CPU for another. Some is healthy; tens of thousands per second usually means too many threads fighting for too few cores or lock contention — the CPU is spending its time switching instead of working. Pair a high cs with pidstat -w to find the culprit. r (the run queue) is the per-instant version of load average’s CPU half: r consistently above your core count is the cleanest possible “CPU is the bottleneck” signal.

The whole point of the CPU section is one binary decision — CPU-bound or I/O-bound? Here is the decision table:

Signal CPU-bound I/O-bound
top %Cpu(s) High us+sy, low id, low wa Low us/sy, high wa, or high id despite high load
vmstat r vs b r high (runnable queue) b high (blocked on I/O)
Load avg composition Mostly R-state tasks Mostly D-state tasks
mpstat Cores near 100% %usr/%sys Cores show %iowait, otherwise idle
The fix lives in… CPU/scheduler tuning, more cores, faster code The disk section, not here

If the evidence says I/O-bound, stop tuning the CPU and jump to the disk section. This redirection is where the method earns its keep.

Memory: the buff/cache truth, swap, and the OOM killer

Memory is where beginners cause the most self-inflicted panic, because the healthy state looks alarming. Let’s fix that permanently.

free -h and the “Linux ate my RAM” myth

# -h = human units; -w widens buff/cache into separate columns on newer procps
free -h
#                total        used        free      shared  buff/cache   available
# Mem:           7.8Gi       5.7Gi       402Mi        18Mi       1.7Gi       1.9Gi
# Swap:          2.0Gi       1.9Gi       108Mi

New operators see free = 402Mi on an 8 GB box and reach for the “add more RAM” button. They are misreading it. Here is what each column actually means:

Column Meaning Should you worry?
total Physical RAM the kernel can use
used RAM in use by processes + kernel, excluding reclaimable cache This is “real” usage
free Completely unused RAM Low is normal and good — unused RAM is wasted RAM
shared tmpfs / shared memory Usually small
buff/cache Page cache + buffers + slab — reclaimable Not “used up”; kernel hands it back on demand
available An estimate of RAM available to new apps without swapping This is the number that matters

The kernel deliberately uses every spare byte of RAM as page cache — copies of recently-read files — so that the next read is a memory hit instead of a disk seek. That cache shows up under buff/cache and drives free toward zero on any box that has been up a while. The moment a process needs memory, the kernel evicts cache instantly and hands it over. So free being tiny is not a leak; it is a warmed cache, which is exactly what you want.

Read available, not free. In the output above, available is 1.9 GiB — the box has headroom. The genuine danger sign is available near zero while swap is churning. Memes about “Linux ate my RAM” exist because this trips up everyone once; now it won’t trip up you.

/proc/meminfo — the source of truth

free is a friendly summary of /proc/meminfo, which has the full detail:

# The kernel's raw memory accounting; free/top/vmstat all read this file
grep -E 'MemTotal|MemFree|MemAvailable|Buffers|^Cached|SwapTotal|SwapFree|Dirty|Writeback|^Slab' /proc/meminfo
# MemTotal:        8151040 kB
# MemFree:          411900 kB
# MemAvailable:    1965300 kB
# Buffers:           18240 kB
# Cached:          1689560 kB
# SwapTotal:       2097148 kB
# SwapFree:         110592 kB
# Dirty:             40960 kB
# Writeback:             0 kB
# Slab:             198320 kB
Field What it is
MemAvailable Kernel’s own estimate of allocatable memory — the honest “how much is left”
Buffers Block-device metadata cache
Cached Page cache — file contents held in RAM
Dirty Modified pages not yet written to disk (see vm.dirty_ratio in tuning)
Writeback Dirty pages being written right now
Slab Kernel data structures (inodes, dentries) — can balloon on file-heavy workloads
SwapTotal/SwapFree Swap capacity and how much remains

Dirty and Writeback are the memory-side view of write pressure; if Dirty is huge, a lot of data is waiting to hit disk and a flush storm may be coming — which connects directly to the vm.dirty_* knobs later.

Swap activity: the number that actually matters

Having swap used is not a problem. Swap being actively read and written — thrashing — is. The distinction is si/so in vmstat:

# Watch swap-in / swap-out. Sustained non-zero si+so = thrashing = real pressure.
vmstat 1 5 | awk 'NR>2 {print "si="$7"  so="$8}'
# si=0  so=0
# si=120 so=80      <- pages moving both ways: the box is thrashing
# si=240 so=16

A box can sit for weeks with 1.9 GB of swap used (stale, cold pages parked there) and be perfectly fast, because nothing is being paged in or out. The instant si/so go sustained-positive, every page fault becomes a disk seek and the machine grinds. Judge swap by si/so, never by “swap used.” sar -W 1 gives the same signal historically (pswpin/s, pswpout/s).

The OOM killer — how the kernel picks a victim

When RAM and swap are both exhausted and a process demands more, the kernel cannot say “no” gracefully — so it invokes the Out-Of-Memory (OOM) killer, which SIGKILLs a process to reclaim memory. The victim is not random. Each task carries an oom_score (0–1000), roughly proportional to its memory footprint, adjustable per-process:

# The current OOM score of a process (higher = more likely to be killed)
cat /proc/3312/oom_score
# 742

# The tunable knob: -1000 (never kill) .. +1000 (kill first). Default 0.
cat /proc/3312/oom_score_adj
# 0

# Protect a critical process (e.g. the database) from the OOM killer:
echo -500 | sudo tee /proc/3312/oom_score_adj

The evidence that an OOM kill happened is in the kernel ring buffer and the journal — this is the log line you grep for when a process “just disappeared”:

# Did the OOM killer fire? This is the smoking gun.
sudo dmesg -T | grep -i -A1 'out of memory'
# [Wed Jul  9 03:14:52 2026] Out of memory: Killed process 3312 (java)
#   total-vm:4820100kB, anon-rss:3100160kB, file-rss:0kB, shmem-rss:0kB, oom_score_adj:0

# Same evidence via the journal, with surrounding context
sudo journalctl -k | grep -i 'killed process'
OOM concept Detail
When it fires RAM + swap exhausted and an allocation can’t be satisfied
How it chooses Highest oom_score — largest memory user, adjusted by oom_score_adj
oom_score_adj −1000 = immune, +1000 = kill first, 0 = default; set via /proc/PID/oom_score_adj
Signal used SIGKILL (9) — the process cannot clean up; it just dies
Where’s the proof dmesg -T / journalctl -k: “Out of memory: Killed process …”
The real fix More RAM, a memory limit (cgroup/MemoryMax=), fixing the leak — not just protecting via oom_score_adj

An OOM kill is an errors signal in USE terms — the memory resource returned a hard failure. If you see it repeatedly, the box is under-provisioned or leaking; oom_score_adj is a seatbelt, not a fix.

swappiness — how eager the kernel is to swap

vm.swappiness (0–100 on most kernels, 0–200 on newer ones; default 60) biases the kernel between reclaiming page cache and swapping out anonymous (application) memory when it needs to free RAM. Lower = “prefer to drop cache, avoid touching app memory.”

vm.swappiness Behaviour Typical use
0 Avoid swapping anon memory until an OOM is imminent Databases, latency-critical (with enough RAM)
1–10 Swap very reluctantly Servers with plenty of RAM; common production choice
60 Default — balanced General-purpose desktops/servers
100+ Swap aggressively, favour keeping cache Rare; memory-overcommitted / some container hosts
# See it, set it live (temporary), then persist it (survives reboot) — details in the tuning section
sysctl vm.swappiness
# vm.swappiness = 60
sudo sysctl -w vm.swappiness=10          # live, lost on reboot
echo 'vm.swappiness = 10' | sudo tee /etc/sysctl.d/99-swappiness.conf   # persistent

Note: swappiness=0 does not disable swap; since kernel 3.5 it means “only swap to avoid OOM.” To truly disable swap you swapoff -a and remove it from /etc/fstab — and then a memory spike goes straight to the OOM killer, so only do that with enough RAM to never need it.

Disk I/O: iostat, the queue, and schedulers

If wa was high or vmstat’s b column had tasks blocked, the disk is your suspect. iostat is the primary instrument.

iostat -xz 1 — the disk’s vital signs

# -x extended stats, -z hide idle devices, 1 = per second. First sample is since-boot; ignore it.
iostat -xz 1
Device    r/s     w/s     rkB/s     wkB/s   rrqm/s  wrqm/s  r_await  w_await  aqu-sz  %util
nvme0n1   12.0   980.0     512.0   62720.0     0.0    45.0     0.42    18.60   12.40   99.80
sda        2.0    30.0      64.0     480.0     0.0     2.0     6.10    22.40    0.85   14.20

Decode the columns that matter (the exact set varies by sysstat version, but these are the load-bearing ones):

Column Meaning USE role
r/s, w/s Read / write operations per second (IOPS) Utilization (workload)
rkB/s, wkB/s Read / write throughput (KB/s) Utilization (bandwidth)
rrqm/s, wrqm/s Requests merged per second (kernel coalescing) Efficiency hint
r_await, w_await Average time (ms) a read/write took, incl. queue wait Saturation — latency
await Combined average I/O latency (ms) Saturation
aqu-sz Average queue depth (outstanding requests) Saturation — the key number
%util Percent of time the device had at least one I/O in flight Utilization (misleading on SSD/NVMe)

Read the example: nvme0n1 is at %util 99.8 — but on an SSD/NVMe that alone means little, because these devices serve many requests in parallel. The real story is aqu-sz 12.4 (twelve requests queued on average) and w_await 18.6 ms (writes taking ~19 ms when the device could do sub-millisecond) — that is a saturated disk. Meanwhile sda at only %util 14 shows w_await 22 ms, telling you it’s a slow spinning disk even when barely busy. This is the USE method in miniature: utilization is the trap; saturation (await, aqu-sz) is the truth.

⚠️ On multi-queue SSDs, do not treat %util = 100% as “disk full/maxed.” Judge by await climbing above the device’s normal latency and by aqu-sz growing. A rising queue with rising latency is saturation; a high %util with flat, low latency is just a busy-but-coping disk.

Which process is doing the I/O? iotop, pidstat -d, lsof

iostat tells you the disk is saturated; it does not say who. Three tools name the culprit:

# iotop — a 'top' for disk I/O. -o shows only processes actually doing I/O.
sudo iotop -o
# Total DISK READ:  1.20 M/s | Total DISK WRITE: 61.00 M/s
#   TID  PRIO  USER   DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
#  3312  be/4  app     0.00 B/s   58.20 M/s  0.00 %  92.4 %  java -jar app.jar
# pidstat -d — per-process disk KB/s, logged over time (scriptable, no curses UI)
pidstat -d 1
# 03:22:10 PM   UID    PID    kB_rd/s   kB_wr/s   kB_ccwr/s  iodelay  Command
# 03:22:11 PM  1001   3312       0.00  59600.00        0.00       42  java
# lsof — which files a process has open (find the file being hammered, or the deleted-but-open one)
sudo lsof -p 3312 | grep -E 'REG' | head
# java  3312  app  200w  REG  259,1  62914560  1179841 /var/log/app/debug.log

lsof also solves the sibling mystery to disk-I/O: df says the disk is full but du finds nothing.” That is a deleted file still held open by a running process — the space isn’t freed until the process closes it. sudo lsof +L1 lists exactly those:

# Files that are deleted but still open (holding disk space hostage)
sudo lsof +L1
# COMMAND  PID USER  FD  TYPE DEVICE SIZE/OFF NLINK  NODE NAME
# app     3312 app   3w   REG  259,1 2.1G      0   1180  /var/log/app/old.log (deleted)

I/O schedulers and mount options

Two disk-side tuning levers are worth knowing. First, the I/O scheduler decides the order requests hit the device:

Scheduler Best for Notes
none Fast NVMe/SSD No reordering — the device’s own queue is smarter; lowest overhead
mq-deadline SATA SSD / HDD, general servers Bounds latency with read/write deadlines; a safe default
bfq Desktop / interactive, mixed Fair queueing, great responsiveness; more CPU overhead
kyber High-IOPS NVMe under load Simple latency-target multiqueue scheduler
# See and set the scheduler (the one in [brackets] is active)
cat /sys/block/nvme0n1/queue/scheduler
# [none] mq-deadline kyber bfq
echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler   # live; persist via a udev rule

⚠️ Setting it via /sys is not persistent. To make it stick, use a udev rule, e.g. /etc/udev/rules.d/60-ioscheduler.rules: ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/scheduler}="mq-deadline".

Second, mount options. The default relatime already avoids most access-time writes, but on read-heavy filesystems noatime eliminates them entirely — no metadata write on every file read:

Mount option Effect Trade-off
relatime Update atime only if older than mtime/ctime or >24h (default) Good balance; rarely needs changing
noatime Never update access time Fastest reads; breaks tools that rely on atime (rare — old mailers)
nodiratime No atime updates for directories only Milder than noatime

Add noatime in /etc/fstab for the read-heavy filesystem and remount. Storage-stack mechanics (partitions, filesystems, /etc/fstab) are covered in the storage lesson; here it is purely a performance knob.

Network performance, briefly

Network tuning is a lesson of its own, but for the “is the network the bottleneck?” question you need three quick reads. Utilization: throughput per interface. Saturation: drops and backlog. Errors: TCP retransmits and NIC errors.

# Socket summary: total sockets, TCP states, and — crucially — retransmit counters
ss -s
# Total: 421
# TCP:   318 (estab 210, closed 40, orphaned 2, timewait 38)

# Per-interface throughput and utilization over time (sysstat)
sar -n DEV 1 3
# IFACE   rxpck/s  txpck/s   rxkB/s   txkB/s   ...   %ifutil
# eth0    18240.0  17980.0  21400.5  20880.1   ...     84.20

# Errors and DROPS per interface (the saturation/error signal)
sar -n EDEV 1 3
# IFACE   rxerr/s  txerr/s  rxdrop/s  txdrop/s  ...
# eth0       0.00     0.00     42.00      0.00

Retransmits are the clearest “the network is hurting” signal — TCP resending data it thinks was lost. See per-socket retransmits with ss -ti, and NIC-level errors/drops with ethtool:

# Per-connection TCP info incl. retransmits (look for 'retrans:' and high 'rtt')
ss -ti | grep -A1 ESTAB | head
#   cubic wscale:7,7 rtt:0.34/0.17 ... retrans:0/128 ...   <- 128 total retransmits on this socket

# NIC hardware/driver stats: errors, drops, and link speed
sudo ethtool -S eth0 | grep -E 'err|drop|discard' | head
sudo ethtool eth0 | grep -E 'Speed|Duplex'
# Speed: 1000Mb/s
# Duplex: Full
Network signal Command Bottleneck if…
Throughput / %ifutil sar -n DEV 1 %ifutil near 100 = link saturated
Drops / backlog sar -n EDEV 1, ss -s rxdrop/s/txdrop/s > 0 = kernel/NIC can’t keep up
Retransmits ss -ti, nstat -az | grep -i retrans Rising retransmits = packet loss / congestion
NIC errors, link ethtool -S, ethtool err/discard counts, or wrong Speed/Duplex

A common trap: high si (softirq) in top on a busy box is the network stack processing packets — pair it with sar -n DEV to confirm the traffic is real.

Sampling deeper: sar history and perf profiling

The tools so far are live. Two more give you history (what happened at 3 a.m. while you slept) and depth (which line of code is burning the CPU).

sar — the flight recorder

sar (from sysstat) periodically records every subsystem to disk, so you can replay any past interval. Enable the collector once:

# Enable historical collection (writes to /var/log/sa/ every ~10 min via a systemd timer)
sudo systemctl enable --now sysstat        # Debian also needs ENABLED="true" in /etc/default/sysstat
# On RHEL/Fedora the timer is: sysstat-collect.timer + sysstat-summary.timer

Then read any metric for any window — the killer feature is -s/-e to zoom into the incident time:

sar flag Reports Answers
sar -u CPU (us/sy/wa/id) Was the CPU busy at 03:00?
sar -r Memory + %memused Was RAM exhausted overnight?
sar -S / -W Swap usage / swap in-out Did it thrash?
sar -b / -d I/O rate / per-device Which disk was hot, and when?
sar -q Run queue + load average history When did load spike?
sar -n DEV/EDEV/TCP Network throughput / errors / TCP Was the link saturated or dropping?
sar -B Paging (pgpgin/s, faults) Page-fault / paging storms
# Yesterday's CPU between 02:00 and 04:00 (sa08 = the 8th of the month)
sar -u -f /var/log/sa/sa08 -s 02:00:00 -e 04:00:00
# Current-day load-average history
sar -q

Being able to say “load hit 40 at 03:12 and CPU iowait was 60% at exactly that minute” — after the fact, without having been watching — is what turns a 3 a.m. mystery into a five-minute diagnosis.

perf — where is the CPU time actually going?

When the CPU is pegged in us and you need to know which function, perf samples the running stacks:

# Install perf: Debian -> linux-tools; RHEL/Fedora -> perf
sudo apt install -y linux-tools-common linux-tools-generic linux-tools-$(uname -r)   # Debian/Ubuntu
sudo dnf install -y perf                                                             # RHEL/Fedora

# Live top-of-CPU by function, like 'top' but for code paths
sudo perf top

# Record with call graphs for 10s, then read the report
sudo perf record -F 99 -a -g -- sleep 10
sudo perf report --stdio | head -30

# Just the summary counters for one command (cache misses, IPC, context switches)
perf stat -- ./my_program
perf subcommand Use
perf top Live “which functions are hot” across the system
perf record -g Capture sampled stacks to perf.data for offline analysis
perf report Explore the recorded profile (often rendered as a flame graph)
perf stat Hardware counters for one run: IPC, cache misses, branch misses

perf samples (cheap, statistical). For tracing every event — every block I/O, every scheduler switch, every slow syscall with full context — you move up to eBPF/bpftrace, covered in Observability with eBPF, bpftrace, perf & ftrace. Reach for that when perf shows you where but you need to know why a specific rare event is slow.

Tuning: tuned, sysctl, and ulimit

Diagnosis names the bottleneck; now you fix it. There are three levers, from highest-level to lowest.

tuned — profiles that flip dozens of knobs at once

tuned is a daemon that applies a named bundle of kernel, scheduler, and device settings. Instead of hand-editing twenty files, you pick a profile that matches your workload and it does the rest — consistently, and re-applied on every boot.

# Install & enable
sudo apt install -y tuned    # Debian/Ubuntu
sudo dnf install -y tuned    # RHEL/Fedora (often preinstalled)
sudo systemctl enable --now tuned

# What profiles exist, and which is active?
tuned-adm list
tuned-adm active
# Current active profile: virtual-guest

# tuned can even recommend one based on the hardware/role
tuned-adm recommend
# virtual-guest

# Switch profile (applies immediately and persists across reboots)
sudo tuned-adm profile throughput-performance
Profile Optimises for Typical use
balanced Compromise of power vs performance Default desktop/laptop
throughput-performance Maximum throughput; disables power saving, tunes VM & I/O Databases, batch, app servers
latency-performance Lowest, most consistent latency; pins C-states Trading, real-time, low-jitter services
network-latency Low network latency (builds on latency-performance) Latency-sensitive network apps
network-throughput Max network throughput (bigger buffers) Bulk transfer, streaming origin
virtual-guest A VM guest (cloud instances) Most cloud VMs — the common default
virtual-host A hypervisor host KVM hosts
powersave Minimum power draw Battery / green

Under the hood, throughput-performance sets things like vm.dirty_ratio, the CPU governor to performance, and a suitable I/O scheduler — you can inspect exactly what any profile does in /usr/lib/tuned/<profile>/tuned.conf. Start here: pick the right profile first, then hand-tune only the specific knob your measurements demand.

sysctl — the individual kernel knobs

sysctl reads and writes kernel parameters exposed under /proc/sys/. The critical distinction — and the one people get wrong — is live vs. persistent:

# READ a value
sysctl vm.swappiness
# vm.swappiness = 60

# WRITE it LIVE — takes effect now, but is LOST on reboot
sudo sysctl -w vm.swappiness=10

# PERSIST it — drop a file in /etc/sysctl.d/ so it re-applies every boot
echo 'vm.swappiness = 10' | sudo tee /etc/sysctl.d/99-tuning.conf
sudo sysctl --system                 # reload all sysctl.d files now

⚠️ sysctl -w alone is a rookie trap: your careful tuning vanishes on the next reboot and the problem returns “mysteriously.” Always mirror a live change into /etc/sysctl.d/*.conf. The knobs you will actually reach for:

sysctl key Default (typical) What it does Tune when
vm.swappiness 60 Bias to swap anon memory vs drop cache Lower (1–10) for DB/latency with ample RAM
vm.dirty_ratio 20 % RAM of dirty pages before writers block and flush synchronously Lower for steadier latency; raise for burst throughput
vm.dirty_background_ratio 10 % RAM of dirty pages that triggers background flush Lower to smooth out write bursts
vm.vfs_cache_pressure 100 How aggressively to reclaim inode/dentry cache Lower (50) to keep metadata cache on file-heavy boxes
net.core.somaxconn 4096 (≥5.4), 128 (older) Max accept queue length per listening socket Raise for high-connection servers (was the classic 128 bottleneck)
net.core.netdev_max_backlog 1000 Packets queued to the stack when the NIC out-runs the CPU Raise on high-PPS boxes seeing rxdrop
net.ipv4.tcp_max_syn_backlog 128–1024 Half-open (SYN) queue size Raise under SYN bursts / connection storms
net.ipv4.ip_local_port_range 32768–60999 Ephemeral source ports for outbound connections Widen on hosts making many outbound connections
net.core.rmem_max / wmem_max ~212 KB Max socket receive/send buffer Raise for high-bandwidth-delay (long fat) networks
fs.file-max (large, RAM-derived) System-wide max open file descriptors Rarely — it’s usually already huge
fs.nr_open 1048576 Per-process hard ceiling for ulimit -n Raise before pushing ulimit -n past ~1M

For the deeper mechanics of /proc, /sys, and how sysctl maps onto kernel parameters (and kernel modules), see Kernel modules, /proc, /sys & sysctl.

ulimit and limits.conf — the “too many open files” fix

The single most common resource-limit error in production is Too many open files (errno EMFILE) — a process hit its file-descriptor cap. FDs are consumed by open files and every socket, so busy network servers hit it constantly. There are three layers, and the classic failure is fixing the wrong one.

# What is the limit in THIS shell? (soft | hard)
ulimit -n            # soft (current) limit, e.g. 1024
ulimit -Hn           # hard (ceiling) limit, e.g. 524288
ulimit -a            # all limits

ulimit only changes the current shell and its children. To persist for interactive/login users, edit /etc/security/limits.conf (enforced by PAM at login):

# /etc/security/limits.conf  — <domain> <type> <item> <value>
*        soft    nofile   65535
*        hard    nofile   65535
app      soft    nofile   200000
app      hard    nofile   200000

The gotcha that costs people hours: limits.conf is applied by PAM at login, so it does not affect services started by systemd — and most production processes are systemd services. Setting limits.conf and restarting your database will not raise its FD limit. The fix lives in the unit file:

# Correct fix for a systemd-managed service
sudo systemctl edit myapp.service
# add:
#   [Service]
#   LimitNOFILE=200000
sudo systemctl daemon-reload && sudo systemctl restart myapp

# Verify the LIVE limit the RUNNING process actually got (don't trust config, check reality):
cat /proc/$(pgrep -f myapp | head -1)/limits | grep 'Max open files'
# Max open files            200000    200000    files
Layer Where Applies to Note
ulimit Shell builtin Current shell + children Temporary; can’t exceed the hard limit unless root
/etc/security/limits.conf PAM config Login sessions (SSH, console) Ignored by systemd services
systemd LimitNOFILE= Unit [Service] That service The correct fix for daemons
fs.nr_open / fs.file-max sysctl Per-process ceiling / system-wide Raise these before pushing limits past ~1M

Always verify against /proc/PID/limits — the running process’s actual limit — rather than trusting what you wrote in a config file. That one habit turns “too many open files” from a recurring outage into a two-minute fix.

Putting it together: a “server is slow” decision walk

Method beats memory. When the page fires, walk the resources in a fixed order and let the numbers eliminate suspects. This is the table to internalise — glance at a handful of counters and it points you at one subsystem:

Symptom (what you see) Likely resource Confirm with Then look at
Load high, top us+sy high, id low, wa low CPU vmstat r > cores; mpstat -P ALL Which process (pidstat), which function (perf top)
Load high but wa high / id high, CPU looks idle Disk I/O vmstat b > 0; iostat -xz await/aqu-sz Which process (iotop, pidstat -d), which file (lsof)
free available near 0, vmstat si/so sustained Memory (swap thrash) vmstat si/so; sar -W Biggest RSS (ps aux --sort=-rss); leak vs under-provision
A process “vanished,” others slow after Memory (OOM) dmesg -T | grep -i oom oom_score, add RAM / MemoryMax=
One core pegged, others idle CPU (single-thread) mpstat -P ALL App concurrency; can’t fix with more vCPUs
High cs in vmstat, latency up CPU (contention) pidstat -w; thread count Lock contention / thread-pool sizing
High st (steal) in top/vmstat Hypervisor (noisy neighbour) top %st; sar -u %steal Resize/move the instance — not a guest fix
App logs Too many open files FD limit /proc/PID/limits; lsof -p count LimitNOFILE= (systemd) or limits.conf
Requests slow, ss -s retrans rising Network sar -n DEV/EDEV; ss -ti retrans ethtool -S; congestion / packet loss

The order matters: check CPU and memory first (cheapest, most common), then disk, then network. And obey the two golden rules — change one thing at a time, and re-measure to prove it moved. If you changed a knob and the number didn’t budge, put the knob back. This decision walk plugs into the broader outage playbook in Troubleshooting methodology: boot, disk, network & permissions — performance is one branch of that tree.

Hands-on lab

A self-contained lab you can run on any Linux VM, cloud instance, container, or WSL2. You will generate a bottleneck in each subsystem, diagnose it with the USE method, and tune one knob. Nothing here destroys data — the only writes go to a scratch file you delete at the end.

⚠️ This lab intentionally loads the machine (CPU, RAM, disk writes). Run it on a throwaway box, not production, and not while others depend on it.

Step 0 — Install the toolkit and a load generator.

# Debian/Ubuntu
sudo apt update && sudo apt install -y sysstat htop iotop stress-ng procps
# RHEL/Fedora/Rocky
sudo dnf install -y sysstat htop iotop stress-ng procps-ng
nproc      # note your core count — you'll compare load against it

What just happened: you have the observe tools (sysstat, iotop) and stress-ng, a synthetic load generator, plus your core count as the yardstick.

Step 1 — Baseline. Know “normal” before you break anything.

uptime; free -h; vmstat 1 3

What just happened: you captured an idle baseline — low load, healthy available memory, si/so = 0, low r/b. Every later reading is compared to this.

Step 2 — Create a CPU bottleneck and diagnose it.

# Pin all cores with busy loops for 30 seconds (run in the background)
stress-ng --cpu $(nproc) --timeout 30s &
# In the same window, observe:
uptime                 # load climbs toward (and past) your core count
vmstat 1 5             # r >= cores, us ~100, wa ~0  -> CPU-bound, user space
mpstat -P ALL 1 3      # every core near 100% %usr
pidstat 1 3            # stress-ng owns the CPU

What just happened: USE verdict = CPU utilization saturated (r ≥ cores, us high, wa low). Because wa is ~0, it’s compute, not I/O. mpstat confirms all cores are used (not a single-thread case).

Step 3 — Create memory pressure and watch swap.

# Allocate more memory than is free to force reclaim/swap (adjust 90% to your box)
stress-ng --vm 2 --vm-bytes 90% --vm-keep --timeout 30s &
vmstat 1 8             # watch free drop, then si/so go NON-ZERO = thrashing
free -h                # 'available' collapses toward 0
dmesg -T | tail -5     # if it goes too far: 'Out of memory: Killed process ...'

What just happened: USE verdict = memory saturatedavailable near 0 and si/so sustained-positive is thrashing. If you pushed harder, the OOM killer fired and dmesg recorded the victim. This is the “Linux ate my RAM” state — but now proven with si/so, not guessed from free.

Step 4 — Create a disk-I/O bottleneck and find the culprit.

# Write a 2 GB scratch file with O_DIRECT so it really hits the disk (not just cache)
stress-ng --hdd 1 --hdd-bytes 2G --timeout 30s &
iostat -xz 1 5         # %util high AND await/aqu-sz climbing = saturated
vmstat 1 5             # b > 0 (blocked tasks), wa rises
sudo iotop -o          # names stress-ng as the writer

What just happened: USE verdict = disk saturated — the giveaway is await/aqu-sz climbing (not %util alone). vmstat’s b column and rising wa corroborate; iotop names the process.

Step 5 — Apply a tuning change and prove it.

# Make swapping less eager (a memory-tuning example), live + persistent
sysctl vm.swappiness                                   # note current (60)
sudo sysctl -w vm.swappiness=10                         # live change
echo 'vm.swappiness = 10' | sudo tee /etc/sysctl.d/99-lab.conf   # persist
sysctl vm.swappiness                                   # re-measure: now 10

# And apply a whole-box profile with tuned (if installed)
sudo tuned-adm profile throughput-performance 2>/dev/null; tuned-adm active 2>/dev/null

What just happened: you changed one knob, persisted it correctly (a file under /etc/sysctl.d/, not just -w), and re-measured to confirm — the full tune-and-verify loop.

Step 6 — Fix “too many open files” the right way.

ulimit -n                                              # this shell's soft limit (e.g. 1024)
# For a systemd service, the ONLY correct fix:
#   sudo systemctl edit myapp   ->  [Service] \n LimitNOFILE=65535
#   sudo systemctl daemon-reload && sudo systemctl restart myapp
#   cat /proc/$(pgrep -f myapp)/limits | grep 'open files'   # verify the LIVE limit
echo "systemd services ignore limits.conf — use LimitNOFILE and verify via /proc/PID/limits"

What just happened: you learned to check the live limit at /proc/PID/limits and to fix daemons in the unit file, not limits.conf.

Step 7 — Clean up.

wait                                                   # let background stress jobs finish
sudo rm -f /etc/sysctl.d/99-lab.conf && sudo sysctl --system   # revert the lab knob
sudo tuned-adm profile balanced 2>/dev/null            # back to default profile
rm -f ./stress-ng* 2>/dev/null; free -h                # confirm memory recovered

What just happened: the box is back to baseline. You have now driven all four USE verdicts and the full tune-and-verify loop end to end.

Common mistakes and troubleshooting

Symptom Cause Fix
“8 GB box, only 400 MB free — out of memory!” Reading free instead of available; buff/cache is reclaimable Read the available column; page cache is not “used up”
Tuned sysctl -w vm.swappiness=10, reverted after reboot Live change never persisted Add it to /etc/sysctl.d/99-*.conf and sysctl --system
Raised limits.conf nofile, service still hits “Too many open files” systemd ignores PAM limits.conf Set LimitNOFILE= in the unit; verify /proc/PID/limits
Added vCPUs, no faster Single-threaded workload — one core pegged Check mpstat -P ALL; fix app concurrency, not core count
Disk shows %util 100% on NVMe — assumed maxed %util is misleading on parallel SSD/NVMe Judge by await and aqu-sz, not %util
Load average 20, CPU shows idle Load counts D-state (I/O) tasks, not just CPU Count R vs D (ps -eo state); look at the disk
df says full, du finds nothing Deleted-but-open file holding space lsof +L1; restart/HUP the process holding it
Guest slow, guest tuning does nothing High %st steal — host oversubscribed Resize/migrate the instance; it’s a host problem
High cs, everything sluggish Context-switch storm: too many threads / lock contention pidstat -w; right-size thread pools
Set I/O scheduler via /sys, gone after reboot /sys writes aren’t persistent Persist with a udev rule

Three gotchas deserve extra words because they cost the most time:

1. %util on SSDs will lie to you. The %util metric was designed for single-spindle rotating disks, where “busy 100% of the time” meant “maxed out.” Modern SSDs and NVMe drives service many requests in parallel, so they can sit at %util 100% while barely working. If you decommission or “fix” a disk because %util is high, you may be chasing a ghost. Always corroborate with await (is latency actually high?) and aqu-sz (is a queue actually forming?). Rising latency + growing queue = real saturation; high %util with flat low latency = a disk that is busy and coping fine.

2. The systemd/limits.conf trap. This one has a near-universal failure story: an engineer hits “Too many open files,” edits /etc/security/limits.conf, restarts the service, and the error comes right back — because the service is a systemd unit, and systemd never consults PAM’s limits.conf. Hours evaporate re-editing a file that was never going to work. Burn this in: daemons → LimitNOFILE= in the unit; login shells → limits.conf; always verify against /proc/PID/limits.

3. Tuning before measuring. The forums are full of “add these ten sysctls for performance” lists. Applying them blind is how you turn one problem into three, none reproducible. Every knob in this lesson has a measurement that justifies it: you lower swappiness because you saw si/so thrashing, you raise somaxconn because you saw accept-queue drops. No measurement, no change. And after every change, re-run the exact command that showed the problem — if the number didn’t move, revert.

Cheat-sheet

Command What it does
uptime / cat /proc/loadavg Load average (1/5/15 min) — compare to nproc
nproc / lscpu Core count / CPU topology (the yardstick for load)
top / htop Live processes; read the %Cpu(s) line (us/sy/id/wa/st)
mpstat -P ALL 1 Per-core CPU — catches single-thread bottlenecks
pidstat 1 Per-process CPU over time (-d disk, -r mem, -w ctx-sw)
vmstat 1 Whole-box: r/b, si/so, bi/bo, cs, cpu split
free -h Memory — read available, not free
`grep -E 'MemAvailable Dirty
dmesg -T | grep -i oom Did the OOM killer fire? Who died?
cat /proc/PID/oom_score{,_adj} OOM victim scoring / protection
iostat -xz 1 Disk: %util, await, aqu-sz, r/s, w/s
iotop -o / pidstat -d 1 Which process is doing the disk I/O
lsof -p PID / lsof +L1 Open files / deleted-but-open (disk-full mystery)
ss -s / ss -ti Socket summary / per-connection retransmits
sar -n DEV 1 / sar -n EDEV 1 Network throughput / errors & drops
ethtool -S eth0 / ethtool eth0 NIC error counters / link speed & duplex
sar -u -f /var/log/sa/saNN Historical CPU (replace -u with -r -b -d -q -n)
perf top / perf record -g Where the CPU time goes, by function/stack
tuned-adm list / active / profile X List / show / apply a tuning profile
sysctl KEY / sysctl -w KEY=V Read / set a kernel knob (live)
echo 'k = v' > /etc/sysctl.d/99-x.conf; sysctl --system Persist a sysctl across reboots
ulimit -n / ulimit -Hn Soft / hard open-file limit (this shell)
systemctl edit svcLimitNOFILE= Raise FD limit for a systemd service
cat /proc/PID/limits The process’s actual live limits (source of truth)

Interview and exam questions

Q: What does load average actually measure, and why can it be high while the CPU is idle? A: It is the exponentially-weighted average count of tasks that are runnable (R) or in uninterruptible sleep (D) over 1/5/15 minutes. Because D-state tasks are blocked on I/O (usually disk/NFS), a saturated disk can drive load high while CPUs sit idle — the demand is for the disk, not the CPU. Always compare load to nproc and check R vs D composition.

Q: free -h shows 200 MB free on an 8 GB server. Is it out of memory? A: Almost certainly not. Look at available, not free. The kernel fills spare RAM with reclaimable page cache (buff/cache), which it releases instantly when apps need it. Low free is healthy; the danger sign is low available plus sustained si/so swap activity in vmstat.

Q: How do you tell a CPU-bound problem from an I/O-bound one? A: In top, CPU-bound shows high us+sy, low id, low wa; I/O-bound shows high wa (or high id despite high load). In vmstat, CPU-bound has a high r (run queue), I/O-bound has a high b (blocked). Confirm with mpstat -P ALL (cores at %usr vs %iowait) and by counting R vs D tasks.

Q: On an NVMe SSD, iostat shows %util 100%. Is the disk maxed out? A: Not necessarily. %util (percent of time with ≥1 I/O in flight) is misleading on devices that serve requests in parallel. Judge saturation by await (latency climbing above normal) and aqu-sz (a growing queue). High %util with flat, low latency is a busy-but-coping disk.

Q: What is the OOM killer, how does it choose a victim, and where’s the evidence? A: When RAM and swap are exhausted, the kernel kills a process (with SIGKILL) to reclaim memory. It picks the highest oom_score — roughly the largest memory user, biased by oom_score_adj (−1000 immune … +1000 first). Evidence is in dmesg -T/journalctl -k: “Out of memory: Killed process …”. The real fix is more RAM or a memory limit, not just oom_score_adj.

Q: You lowered vm.swappiness with sysctl -w but it reset after a reboot. Why, and how do you make it stick? A: sysctl -w changes the live kernel only; it isn’t persistent. Put vm.swappiness = 10 in a file under /etc/sysctl.d/ (e.g. 99-tuning.conf) and run sysctl --system — those files are applied on every boot.

Q: A service keeps logging “Too many open files.” You raised nofile in /etc/security/limits.conf but nothing changed. What’s wrong? A: limits.conf is enforced by PAM at login and does not apply to systemd-managed services. Set LimitNOFILE= in the unit (systemctl edit svc), daemon-reload, restart, and verify the live limit with cat /proc/PID/limits.

Q: What does tuned do, and name three profiles and when you’d use them. A: tuned applies a named bundle of kernel/scheduler/device settings and re-applies it on boot. throughput-performance for databases/batch (max throughput, no power saving); latency-performance for low, consistent latency (real-time/trading); virtual-guest for cloud VMs. Apply with tuned-adm profile <name>; check with tuned-adm active.

Q: Decode these vmstat columns: r, b, si, so, wa, cs. A: r = runnable tasks (CPU demand; > cores = saturated); b = tasks blocked in uninterruptible I/O; si/so = pages swapped in/out per second (sustained non-zero = thrashing); wa = CPU % idle waiting on I/O; cs = context switches/sec (very high = contention or too many threads).

Q (RHCSA-style): Persistently set vm.swappiness to 10 and verify. A:

echo 'vm.swappiness = 10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system
sysctl vm.swappiness      # -> vm.swappiness = 10

Q (RHCSA-style): Show current-day CPU history, then just the window 02:00–03:00. A:

sar -u                                    # today's CPU, all recorded intervals
sar -u -s 02:00:00 -e 03:00:00            # zoom to the incident window
# (requires sysstat collection enabled: systemctl enable --now sysstat)

Q: Which single command best summarises CPU, memory, swap, and I/O together, and how do you read it? A: vmstat 1. Ignore the first line (since-boot average). Then read r/b (CPU vs I/O demand), si/so (swap thrash), bi/bo (disk throughput), cs (context switches), and the us/sy/id/wa/st CPU split — the whole machine’s health in one line per second.

Key takeaways

linuxperformancetuninguse-methodvmstatiostatsartunedsysctlulimitoom-killerswappinesspidstatrhcsa
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments