Production debugging with no repro: perf, eBPF, flame graphs, and why ptrace stops the world
A wedged prod process has no repro and you cannot stop it. Sampling tools — perf, eBPF/bpftrace, async-profiler — observe at 1-5% overhead without halting; a ptrace attach stops the process. USDT probes and flame graphs locate the hot path on a live, untouched service.
One pod was pinned at 100% CPU, serving requests but slowly, and it had been that way for an hour. The on-call engineer’s instinct was to attach gdb and get a stack — and a senior stopped his hand. “That ptrace attach stops the process. It is taking traffic; you will freeze a live server and the load balancer will pile a thousand requests onto its siblings while you read one stack.” Instead they ran perf record -F 99 -p <pid> for ten seconds — sampling the running process 99 times a second at roughly 2% overhead, never halting it — and folded the result into a flame graph. The wide plateau at the top was unmistakable: 80% of CPU in a regex compile, called per-request because a cache key had been computed wrong and the compiled pattern was never reused. They never stopped the process, never dropped a request, and had the answer in under a minute. The lesson the junior took away: in production the first question is not “what is the bug” but “can I observe this without stopping the world”.
The production constraint: you cannot stop the world
Every debugger technique from the previous lessons assumes you can halt the process. In production that assumption inverts. The classic interactive debugger attaches via ptrace, and a ptrace attach (PTRACE_ATTACH, what gdb and Delve do) stops every thread of the target until you detach. On a live server taking traffic that is catastrophic: requests queue or time out, health checks fail, the load balancer ejects the instance and dog-piles its siblings, and an autoscaler may kill the “unhealthy” pod you were debugging — destroying the live state you attached to read. So the production toolkit is built on a different principle: observe without stopping. You sample or trace the process as it runs, paying a few percent overhead, and you reconstruct what it is doing from those samples rather than freezing it to inspect.
There is a second reason ptrace is the wrong default in prod: it is single-target and intrusive, while the questions are usually statistical — “where does CPU go”, “what syscall is slow”, “which path allocates” — answered far better by aggregating thousands of cheap samples than by one frozen snapshot.
Sampling vs instrumenting: perf, eBPF, async-profiler
When you pick up a production observability tool, the first decision — sampling or instrumenting — determines whether your overhead is predictable or whether you accidentally crush the service you’re trying to diagnose. Getting this wrong is how a “quick look” turns into an incident of its own.
The low-overhead toolkit splits along sampling versus instrumenting. Sampling interrupts the process at a fixed frequency — perf record -F 99 takes 99 stack samples per second per CPU — and infers where time goes from the distribution; overhead is roughly 1-5% because most cycles run untouched and only the periodic interrupt costs anything. Instrumenting attaches to specific events — a function entry, a syscall, a tracepoint — and runs on every occurrence; cheap per-event but costly if the event is hot. perf, the kernel’s sampler, profiles CPU with hardware performance counters. eBPF (driven by bpftrace) runs a verified, sandboxed program in the kernel on chosen events, aggregating in-kernel so even per-event tracing stays cheap — bpftrace -e 'tracepoint:syscalls:sys_enter_read { @[comm] = count(); }' counts reads by process with negligible cost, no process stop, no kernel module. For managed runtimes, async-profiler samples the JVM (or similar) using a signal-based or perf-events mechanism that, unlike naive JVMTI stack-walking, does not suffer safepoint bias — it can sample threads at arbitrary points, not just where the runtime parks them, so the flame graph reflects real hot paths rather than safepoint locations.
A pod is pinned at 100% CPU and serving live traffic. The on-call wants to gdb-attach to grab a stack. Why is that the wrong first move, and what is better?
▸Why this works
Why does sampling at 99 Hz reveal a hot path so reliably when it looks at the process only ~99 times a second? Because hotness is statistical. If 80% of CPU time is spent in one function, then across thousands of samples roughly 80% will catch the stack inside that function — the wide plateau in the flame graph. You are not trying to see every instruction; you are estimating a distribution, and a distribution converges fast with cheap samples. The odd 99 (not 100) avoids lock-step with timers that tick at round frequencies, which would systematically miss or over-count periodic work. Cheap, slightly-irrational sampling beats expensive exhaustive tracing for the “where does time go” question.
USDT probes and flame graphs: locating the path live
Two pieces turn samples into an answer. Flame graphs aggregate thousands of stack samples into a single picture: each box is a function, width is the share of samples (time), stacked by call depth. A wide box near the top is where CPU actually goes — the regex-compile plateau from the Hook reads off in one glance, no stepping required. USDT probes (userland statically-defined tracing) are zero-overhead-when-disabled markers compiled into a binary (databases, language runtimes, your own service) that eBPF/perf can attach to at named, semantically meaningful points — “transaction commit”, “query start” — giving you stable instrumentation without modifying or stopping the process. Together they let you answer “what is this wedged process doing right now” on a live, untouched service: sample it, fold into a flame graph, read the hot path, and confirm with a USDT or kernel tracepoint that the suspect path is the one firing. The whole investigation costs a few percent CPU and zero downtime, which is the only kind of debugging a production SLA allows.
You want to count how often a specific syscall fires across all processes on a busy prod node, with minimal overhead and no process stops. Which approach fits, and why?
- 01Why is a ptrace/gdb attach the wrong default in production, and what replaces it?
- 02Distinguish sampling from instrumenting, and explain how a flame graph and USDT probes locate a hot path live.
Production debugging inverts the core assumption of an interactive debugger: you cannot stop the world. A ptrace attach — PTRACE_ATTACH, what gdb and Delve perform — halts every thread of the target until detach, and on a live server taking traffic that means queued requests, failed health checks, the load balancer ejecting the instance and dog-piling its siblings, and an autoscaler potentially killing the very pod whose live state you attached to read. ptrace is also single-target and intrusive when the real production questions are statistical: where does CPU go, which syscall is slow, which path allocates. So the production toolkit observes without halting. Sampling interrupts the process at a fixed frequency — perf record -F 99 takes 99 stack samples per second — at roughly 1-5% overhead, because most cycles run untouched and only the periodic interrupt costs; it estimates a distribution, which converges fast, and the odd 99 avoids lock-step with round-frequency timers. Instrumenting attaches to specific events and runs on each, exact for counting but costly on hot events; eBPF driven by bpftrace runs verified sandboxed programs on kernel events and aggregates in-kernel, so even per-event tracing stays cheap, stops nothing, and needs no kernel module. async-profiler samples managed runtimes like the JVM without safepoint bias, so flame graphs reflect real hot paths, not safepoint locations. Two pieces turn samples into an answer: flame graphs fold thousands of stacks into one picture where box width is time share, so the widest top plateau is where CPU goes — the Hook’s per-request regex compile read off in a glance; and USDT probes, zero-cost-when-disabled markers compiled at meaningful points, let eBPF/perf confirm the suspect path on a live, untouched service. The whole investigation costs a few percent CPU and zero downtime — the only debugging a production SLA permits, and the reason the first prod question is not what is the bug but can I observe this without stopping the world.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.