Put the whole course together — take a realistic slow-service mystery from symptom to root cause, reaching for the right eBPF tool at each narrowing step.
🎯 Mission: turn "it's slow and I don't know why" into a named root causeReal debugging is a drill-down: start broad — is the time spent on-CPU, on disk, or waiting? — then apply the right eBPF tool at each narrowing step until you can name the root cause.
It's Tuesday. Your payments API is mostly fine — p50 latency is a healthy 40 ms — but support keeps forwarding the same complaint: "every so often a request just hangs for a couple of seconds." The dashboards are green. CPU looks normal. There's nothing useful in the logs. You can't reproduce it on your laptop.
This is the exact situation eBPF was made for. The service is already running in production, you can't add log lines and redeploy on a hunch, and the interesting behavior is intermittent. Instead of guessing, you're going to ask the running kernel precise questions until the mystery collapses into one sentence: "requests are slow because ______."
Throughout, every command is a Linux-only "try later" one-liner. You won't run anything here; you're learning to read the investigation and know which tool to reach for.
The single biggest mistake under pressure is reaching for a favorite tool and hoping. The fix is a methodology — a fixed procedure that tells you what to look at first, so you stop guessing. The classic one is Brendan Gregg's USE method.
For every resource (CPU, memory, disks, network interfaces), check three things:
"The average time the resource was busy servicing work." A disk at 100% util is doing all it can.
"The degree to which the resource has extra work it can't service, often queued." Saturation is where waiting (latency) is born.
"The count of error events." Cheap to check, so check them first — a retry storm or failing disk hides here.
Gregg's claim is that this simple loop "solves about 80% of server issues with 5% of the effort." It doesn't tell you the answer — it tells you which resource to interrogate next, so your eBPF tools point at the right place.
Source: Brendan Gregg — The USE Method · methodology & tooling expanded in BPF Performance Tools.
USE tells you to interrogate resources; the question that makes it concrete is "is my latency on-CPU or off-CPU?" That one split, then count-then-snoop, is the whole flowchart:
Before any clever one-liner, answer the cheapest, most decisive question: when a request is slow, is the CPU busy doing work, or is it sitting idle while the request waits? This one split sends you down completely different paths.
| On-CPU (burning cycles) | Off-CPU (waiting / blocked) | |
|---|---|---|
| What's happening | The thread is running, computing — a hot loop, JSON churn, a regex, crypto. | The thread is parked by the scheduler, waiting on disk, a lock, the network, or a sleep. |
| CPU looks like | High & busy | Often idle — the giveaway: slow but CPU isn't pegged. |
| Reach for | Sampling profiler → flame graph (Lesson 7) | Latency histograms / off-CPU timing (Lesson 6) |
| It answers | "Which function is eating the CPU?" | "What is it waiting on, and for how long?" |
In our scenario the tell is loud: support says requests hang, and the CPU dashboards are green the whole time. Slow, but CPU is idle → this is off-CPU. Something is making the request wait. If instead the CPU had been pegged during slow requests, we'd grab a profiler and a flame graph (the on-CPU tool from Lesson 7) to see which function is hot. Here, we follow the off-CPU branch.
On-CPU vs off-CPU framing per Brendan Gregg — Linux eBPF Tracing Tools and the USE method.
We're on the off-CPU branch: requests wait. Now we narrow. The pattern is always the same two moves — count to find the hotspot, then snoop to see the detail. Three one-liners, each eliminating a guess.
First, a wide-angle shot. Count every syscall the box makes, grouped by syscall name, so an abnormal one jumps out. @[probe] uses the firing probe's full name as the map key; count() tallies it (the map idea from Lesson 5).
# count every syscall entry, grouped by syscall name sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
sudo). Ctrl-C to stop; bpftrace prints the map on exit.Expected output (sorted ascending, biggest last):
# ... long tail trimmed ... @[tracepoint:syscalls:sys_enter_read]: 14233 @[tracepoint:syscalls:sys_enter_write]: 15901 @[tracepoint:syscalls:sys_enter_openat]: 288417 <-- whoa
What it tells you: openat — the syscall that opens files — is firing wildly more than anything else. A payments API should not be opening a quarter-million files. That's the thread to pull. (You could also key by @[comm] to see which process is responsible — the Lesson 3 builtins.)
Counting found the suspicious syscall; now snoop it to see the actual filenames. This is the open-tracing one-liner you met early on (probes & arguments, Lesson 4): comm is the process name, args.filename is the path it's opening, and str() turns that kernel pointer into readable text.
# show every file open, system-wide: who opened what sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%-16s %s\n", comm, str(args.filename)); }'
sudo).Expected output (streaming live):
payments-api /etc/app/config.d/feature-flags.json
payments-api /etc/app/config.d/feature-flags.json
payments-api /etc/app/config.d/feature-flags.json
payments-api /etc/app/config.d/feature-flags.json
# ... the same path, hundreds of times a second ...
What it tells you: the service is hammering one config file — feature-flags.json — over and over. A file it opens this often is almost certainly missing or unreadable, so a hot code path keeps retrying. That's a retry storm. (Confirm with the open-failure angle: trace tracepoint:syscalls:sys_exit_openat and check args.ret < 0 — a negative return is a failed open, e.g. -2 = ENOENT, "no such file.")
Why does a retry storm make requests hang rather than just spin CPU? Because each open touches the filesystem, and if the disk is slow, every retry blocks off-CPU. Confirm with a latency histogram (the distribution idea from Lesson 6). The idiom: stash a timestamp keyed by thread on the entry probe, then on the matching return probe record now − start into a hist().
# VFS read latency as a power-of-2 histogram (nanoseconds) sudo bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /has_key(@start, tid)/ { @ns = hist(nsecs - @start[tid]); delete(@start, tid); }'
sudo). tid = thread id; nsecs = a nanosecond clock. For block-device latency specifically, the ready-made tool is biolatency.Expected output (a bimodal histogram — the smoking gun):
@ns: [1K, 2K) 48201 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@| [2K, 4K) 9930 |@@@@@@@@@@ | # ... fast page-cache reads above ... [16M, 32M) 1422 |@ | <-- ~16-32 ms [32M, 64M) 611 | | <-- ~32-64 ms
What it tells you: most reads return in microseconds (served from cache), but a long tail takes tens of milliseconds — real disk hits. Stack a few dozen of those per slow request (the retry storm) and you get exactly the multi-second hang support reported. Two findings now interlock.
One-liner syntax verified against the bpftrace One-Liner Tutorial and bpftrace language reference (@[probe]=count(), str(args.filename), the @start[tid]=nsecs timing idiom). Recipes from BPF Performance Tools.
The drill-down is done. Lay the evidence end to end and the one-sentence answer writes itself:
| Step | Tool | What it showed |
|---|---|---|
| Characterize | on/off-CPU split | Slow but CPU idle → off-CPU, it's waiting. |
| Count | @[probe]=count() | openat is firing absurdly often. |
| Snoop | openat tracer | Same missing config file, retried in a hot loop. |
| Measure | read-latency hist() | Long tail of ~16–64 ms disk reads. |
feature-flags.json that a hot code path retries on every request; each retry is a filesystem open that occasionally hits the slow disk (16–64 ms), and a burst of retries stacks into a multi-second stall."Fix hypothesis (a hypothesis — you confirm by deploying and re-measuring):
openat count and the read-latency histogram. The openat count should crater and the slow tail should vanish. Same tools, now as a regression check.This whole investigation was just earlier lessons used in order. The capstone added no new tool — only the sequence:
On-CPU? → profile + flame graph (see Lesson 7). Off-CPU / waiting? → latency histograms (see Lesson 6).
Aggregating with maps — @[probe] = count() — is the core of Lesson 5. Cheap, wide, points at the offender.
Reading comm and arguments came from your first one-liner (Lesson 3) and choosing probes & reading their args (Lesson 4).
The @start[tid]=nsecs → hist() timing idiom is timing & latency (Lesson 6); BCC tools like biolatency live in Lesson 8.
Why you can run all of this live without fear — overhead, the verifier, blast radius — is Lesson 9.
The whole "ask the running kernel a question" model started in Lesson 1 — events, the verifier, maps. Everything since is detail on that.
A fresh mystery lands on you. Walk the drill-down in your head before peeking:
Notice: the procedure is identical to the capstone — only the first branch flipped from off-CPU to on-CPU. That's the whole game.
You've finished the conceptual arc. Two ways to turn it into a durable skill:
The fastest way to cement this: take an actual slow/flaky/mysterious thing you've hit — at work or in a side project — and we'll walk the drill-down together. Which branch? Which one-liner? I'll help you read the output and name the cause.
Everything here is run-it-later until you have Linux. A cloud VM or a local Linux VM unlocks running these one-liners for real. Ask me for a vetted "Linux on a Mac" setup when you're ready (it's a noted gap in RESOURCES.md).
When you want to pressure-test your understanding against real practitioners: the eBPF community Slack (newcomer + bpftrace channels) and r/eBPF. See RESOURCES.md for all of it.
For a subsystem-by-subsystem cookbook of debugging recipes, BPF Performance Tools is the definitive next step — 150+ tools organized exactly like this drill-down.
Communities & reading per RESOURCES.md and ebpf.io — Get Started.
Scenario questions — pick the right tool for each. Instant feedback, no grading.
@[probe] = count() shows openat firing 200× more than any other syscall. What does that spike most suggest?Click to reveal. Quick review of the capstone's load-bearing ideas.
@[probe] = count()) — cheap and wide, it finds the hotspot. Then snoop just that offender to see the detail (filenames, args). Snooping first would be a firehose with no signal.hist() reveals the shape — e.g. a bimodal split where most reads are fast (cache) but a long tail hits slow disk (tens of ms). Averages hide that tail; histograms expose it.This is the capstone, so let's make it stick: bring me a real "why is this slow?" you've actually hit and we'll walk the drill-down together. Or try: "Re-run this investigation but for a memory leak — which branch and which tools?" or "What would the flame-graph version of Step 1 have looked like?"