eBPF Course · Lesson 9

Running It Safely in Production

Use eBPF tracing on a live system with confidence — manage overhead, meet the requirements, and know its limits.

🎯 Trace the real production box — without being the outage
The one idea

eBPF tracing is safe by design — but it isn't free. Manage probe overhead, mind the kernel and permission requirements, and know the handful of things it genuinely can't see.

01 Why tracing is read-mostly & safe

You've spent eight lessons learning to ask the running kernel questions. The natural next worry for an app developer is: "Can I really point this at the production box without taking it down?" The honest answer is yes, with care — and this lesson is the "with care."

Start with why it's safe at all. Recall the verifier from Lesson 1: before any eBPF program runs, the kernel statically proves it will terminate, never reads uninitialized memory, and never touches memory out of bounds. A program that fails verification is rejected at load time — it never gets to run. There is no eBPF equivalent of "a bad tracer segfaulted the kernel." As the official docs put it, the verifier ensures programs "always run to completion" and "may not access memory out of bounds."

The second reason is the nature of observability programs specifically. The tracing tools you've met — opensnoop, execsnoop, your own bpftrace one-liners — are read-mostly. They attach to an event, read some state (a syscall argument, a timestamp, a stack), and write a number into a map. They don't change what the kernel does. They observe.

💡 Two different verbseBPF can be used to observe (tracing — read state, count, time) or to act (drop a packet, block a syscall — networking/security, out of scope for this course). This whole course lives in the observe half, which is exactly the half that is safe to run live.

Source: ebpf.io — What is eBPF? (the verifier & sandboxing) · docs.kernel.org — the BPF verifier

02 Where overhead actually comes from

"Safe" and "free" are different claims. Every time your probe fires, the kernel runs a little extra code. One probe firing a hundred times a second is invisible. The same probe firing two million times a second is a tax you can feel. Overhead is driven almost entirely by one thing: how often the probe fires × how much it does each time.

① Event frequency

The big one. A probe on a quiet path (process exec, file open) fires rarely. A probe on a hot path — the scheduler context-switch, a per-packet hook, a malloc wrapper — can fire millions of times a second. Cost scales with the firing rate.

② Per-event output

Printing one line per event to user space (a perf buffer) means crossing the kernel/user boundary constantly. On a hot path that flood alone can dwarf the probe's real work. (Tie back to Lesson 5: this is the printf-per-event trap.)

③ uprobe cost

User-space probes (uprobes) are pricier per hit than kernel probes — each one is a trap into and back out of the kernel. Fine for a function called occasionally; expensive on something called in a tight loop.

④ Copying work

Grabbing a string (a filename, a query) or walking a stack trace on every event is real work — copying bytes, unwinding frames. Cheap once; costly a million times. Capture it only when you need it.

⚠ The phrase to rememberThere is no such thing as "an expensive probe" in the abstract — there are expensive places to put a probe and expensive things to do on each hit. Same one-liner is free on execve and ruinous on the scheduler.

Source: Brendan Gregg — Linux eBPF Tracing Tools (per-event perf output vs. summary maps).

03 The golden rule: aggregate in the kernel

Here is the single most important production habit, and it's the lesson Brendan Gregg repeats throughout his tools: do the math inside the kernel and ship out a summary, not a stream.

Instead of sending one line per event to user space and counting them there, you keep a map (a histogram, a counter) in the kernel, update it on each event, and let your tool read the finished summary periodically. A million events become a handful of bucket counts. In Gregg's words, "the summarization is all done in kernel context, for efficiency."

ApproachWhat crosses to user spaceCost on a hot path
Per-event — print/perf-buffer every hit One record per event (millions/sec) Expensive — boundary crossings flood the system
In-kernel aggregate — a map / histogram, read on a timer One small summary per second (or on exit) Cheap — the firehose stays in the kernel

In bpftrace this distinction is right there in the syntax. The right-hand version below builds the histogram in-kernel and only prints it when you hit Ctrl-C:

# ✗ per-event: one printed line for EVERY read syscall — a flood on a busy box
bpftrace -e 'tracepoint:syscalls:sys_enter_read { printf("%s %d\n", comm, args.count); }'

# ✓ in-kernel aggregate: build a histogram of read sizes, print ONCE at exit
bpftrace -e 'tracepoint:syscalls:sys_enter_read { @bytes = hist(args.count); }'
Linux-only — copy and try later on a Linux box (needs sudo).

Three more moves keep you on the cheap side:

# Sampling, not tracing: 99 stacks/sec per CPU — fixed cost, ideal for hot CPU paths
bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'
Linux-only — copy and try later on a Linux box (needs sudo).
💡 Why 99 and not 100?Brendan Gregg samples at 99 Hz on purpose: an odd, off-round rate avoids "lockstep" — sampling in phase with something that ticks at exactly 100 Hz, which would bias your results.

Source: Brendan Gregg — eBPF tools & in-kernel summarization · Gregg on sampling profiles (99 Hz).

04 The overhead spectrum — and how to move left

Every tracing decision lands somewhere on a dial. The left end is cheap enough to leave running on production all day; the right end can perturb the very thing you're measuring. The good news: a few deliberate choices slide you leftward.

CHEAP — safe to leave running EXPENSIVE — can perturb the system 99 Hz profiling fixed cost Aggregated tracepoints Well-filtered probe (predicate) Per-event print on busy path Unfiltered uprobe, hot path + stacks these moves slide you LEFT (cheaper) To move left: • Aggregate in-kernel (maps/histograms) instead of per-event output • Add a predicate to filter early — drop uninteresting events in the probe • Sample (profile) instead of trace when chasing a hot CPU path • Avoid hot-path probe points; skip string/stack copies unless you need them
The overhead dial: green = cheap (aggregated, sampled, filtered), orange = expensive (unfiltered per-event tracing of hot paths with string/stack copies). The same one-liner can sit at either end depending on these four choices.

05 Bounding event floods

The failure mode that actually bites people isn't a crash — it's a flood. You attach a printing one-liner to a busy path, and suddenly your terminal (and the perf buffer feeding it) can't keep up. Two things happen: your output scrolls uselessly, and the kernel starts dropping events because the buffer is full. bpftrace will even warn you it lost events.

You bound a flood the same way you cut overhead — by reducing how many records leave the kernel:

Predicate (filter)

Scope the probe to what matters: one process, one file, slow cases only. Fewer events fire the action at all.

Aggregate

Count or histogram in-kernel and print a summary. A flood of events collapses to a few numbers.

Sample

Take 1-in-N or a fixed-rate sample instead of every event. The picture survives; the volume doesn't.

# Flood-prone: prints EVERY scheduler switch — a busy box drowns instantly
bpftrace -e 'tracepoint:sched:sched_switch { printf("%s\n", comm); }'

# Bounded: predicate (one process) + aggregate (count), summary at exit only
bpftrace -e 'tracepoint:sched:sched_switch /comm == "nginx"/ { @[comm] = count(); }'
Linux-only — copy and try later on a Linux box (needs sudo).
💡 Dropped events are a signal, not just noiseIf bpftrace tells you it lost events, that's the tool warning you the firehose is too big for the channel — tighten the predicate, switch to an aggregate, or sample. Don't ignore it and trust the partial numbers.

06 Requirements: kernel version, BTF & CO-RE

eBPF tracing is a kernel feature, so the kernel underneath you decides what you can do. There's no install-a-newer-eBPF; you get what your kernel ships.

Kernel version

Modern eBPF tracing wants a reasonably recent kernel. bpftrace runs on roughly 4.9+, but the comfortable floor for the full toolkit is the 5.x series, where the important pieces below are mainstream. Newer kernels keep adding probe types and builtins (e.g. richer attach points, BTF-typed tracepoints). Rule of thumb: the older the kernel, the fewer probes and the more fiddly it gets.

BTF and CO-RE — the portability story

BTF (BPF Type Format) is compact type information describing the kernel's own data structures — the layout of every struct, encoded in a space-efficient form the kernel can carry at runtime. When built with CONFIG_DEBUG_INFO_BTF, the kernel exposes its BTF at /sys/kernel/btf/vmlinux.

Why an app developer should care: BTF is what powers CO-RE (Compile Once – Run Everywhere). Kernel struct layouts shift between versions, so historically a tracing tool had to compile against the exact kernel on each machine (the old BCC model, which dragged a compiler onto every host). With CO-RE, a program is compiled once with relocation hints, and the loader matches those hints against the target kernel's BTF at load time — adjusting field offsets so the same binary runs across many kernels, no on-box compiler needed.

🎯 What this buys youBTF/CO-RE is why a modern tracing agent can be one small binary you drop onto a fleet of differently-versioned machines and have it "just work." For your bpftrace one-liners, BTF also means you can reference kernel struct fields by name and get readable output — without hand-copying kernel headers.
CapabilityNeeds a newer-ish kernelWhy it matters to you
Basic kprobes / tracepointsOld (4.x) is usually fineMost "what's calling X / what's slow" one-liners work
BTF exposed by the kernel5.x mainstream (CONFIG_DEBUG_INFO_BTF)Readable struct fields; foundation for CO-RE
CO-RE portable binaries5.x + BTFOne agent binary across many kernels
Newest probe types & helpersWhatever added themSome advanced tracing simply isn't there on old kernels
# Quick checks on a Linux box: kernel version, and whether BTF is present
uname -r
ls /sys/kernel/btf/vmlinux   # exists ⇒ kernel BTF available (CO-RE-ready)
Linux-only — copy and try later on a Linux box (the BTF check needs no sudo; tracing does).

Source: docs.kernel.org — BPF Type Format (BTF) · BPF CO-RE: portability across kernels · ebpf.io.

07 Permissions: who is allowed to trace

Loading an eBPF program is a privileged operation — the kernel won't let just any process attach code to itself. For an app developer, this is the part most likely to surprise you, because "it works on my laptop as root" hides what's really required.

The capabilities

⚠ Unprivileged eBPF is disabled by defaultYou may have heard eBPF can run "unprivileged." In practice, don't count on it. Unprivileged BPF is heavily restricted, and major distributions (Ubuntu, SUSE, and others) ship with it turned off by default for security hardening — controlled by the kernel.unprivileged_bpf_disabled sysctl. Assume you need privileges (root, or CAP_BPF+CAP_PERFMON) to trace.

Containers & Kubernetes

This is where app developers get tripped up. There is one kernel, shared by the host and every container on it — containers are isolated processes, not separate kernels. So eBPF tracing reaches across that boundary, but loading a probe still needs host-level privilege:

Shared host kernel

A probe loaded from a pod sees the whole host — all processes, all containers. Powerful, and exactly why it needs privilege.

Privileged / capabilities

A pod that traces typically needs to be privileged (or granted CAP_BPF + CAP_PERFMON / CAP_SYS_ADMIN) — ordinary pods can't load probes.

hostPID & visibility

To map kernel events back to the right processes you often need hostPID: true so the pod sees host PIDs, not just its own.

Node-level agent

The clean pattern: a DaemonSet — one privileged tracing agent per node — rather than privilege scattered across app pods.

Source: eBPF Docs — CAP_BPF / CAP_PERFMON (Linux 5.8) · LWN — Introducing CAP_BPF · docs.kernel.org — unprivileged_bpf_disabled sysctl.

08 What eBPF genuinely cannot do

Confidence cuts both ways: knowing the limits keeps you from chasing answers eBPF can't give. None of these are bugs — they're inherent.

"If it runs, eBPF can trace it."

No probe point, no visibility. eBPF attaches to existing hooks — tracepoints, kprobes on kernel functions, uprobes on user functions. If there's no place to attach, there's nothing to see.

"Every function is traceable."

Inlined / optimized-away functions vanish. The compiler may inline a small function into its caller — at runtime it isn't a separate function, so there's no entry point to probe.

"Stack traces are always reliable."

Missing symbols & broken unwinding. Stripped binaries, no frame pointers, or absent debug info give you addresses without names, or truncated stacks. The data's only as good as the symbols.

"eBPF replaces my metrics & logs."

It complements them. eBPF sees system behavior (syscalls, latency, scheduling) brilliantly, but it doesn't know your business meaning — "this is checkout step 3, for customer X." That's what app metrics, logs, and traces are for.

💡 The right framingeBPF is the x-ray of the running system: unmatched for "what is the kernel/process actually doing right now," with no code change and no restart. It sits alongside your application metrics, logs, and distributed traces — each answers questions the others can't.

Source: Brendan Gregg — eBPF tracing tools & limitations · ebpf.io — hooks & program model.

09 Check yourself

You want to know where CPU time goes inside a process that runs a function millions of times per second. What's the cheapest, safest way to get the answer?
On a modern (5.8+) kernel, what is the minimal set of capabilities needed to load a tracing eBPF program without full root?
Which of these is a genuine, inherent limitation of eBPF tracing?
Why is "build a histogram in a map and read it once per second" so much cheaper than "printf one line per event" on a busy path?

10 Flashcards

Q: What two factors drive eBPF tracing overhead?
A: How often the probe fires × how much it does each hit. Hot paths (scheduler, per-packet, tight-loop uprobes) fire millions of times/sec; per-event output, string copies, and stack walks add per-hit cost. A probe is only "expensive" because of where it's placed and what it does.
Q: What's the golden rule for cheap, production-safe tracing?
A: Aggregate in the kernel. Keep a map/histogram updated on each event and ship out a periodic summary — instead of one record per event. A million events collapse to a few bucket counts; the firehose never crosses to user space. Also: filter early with predicates, and prefer sampling over tracing for hot paths.
Q: Since Linux 5.8, which capabilities let you load a tracing program without root?
A: CAP_BPF + CAP_PERFMON. CAP_BPF allows the bpf() syscall and creating maps; CAP_PERFMON allows attaching to tracing/perf points (tracepoints, kprobes, perf events). (Networking programs use CAP_BPF + CAP_NET_ADMIN instead.) Unprivileged eBPF is disabled by default on most distros, so assume you need privilege.
Q: What are BTF and CO-RE, and why do they matter?
A: BTF (BPF Type Format) is compact kernel type info, exposed at /sys/kernel/btf/vmlinux when built with CONFIG_DEBUG_INFO_BTF. CO-RE (Compile Once – Run Everywhere) uses it so one compiled program adapts its field offsets to whatever kernel it loads on — one binary runs across many kernel versions, no on-box compiler. Mainstream on 5.x kernels.
Q: What's the key caveat when tracing in Kubernetes / containers?
A: There's one shared host kernel — containers aren't separate kernels. A probe sees the whole host, so loading one needs host-level privilege: a privileged pod (or CAP_BPF+CAP_PERFMON), often hostPID: true to resolve host PIDs. The clean pattern is a node-level DaemonSet agent, not privilege scattered across app pods.
Q: Name things eBPF genuinely cannot see.
A: Anything with no probe point (no tracepoint/kprobe/uprobe to attach to); functions the compiler inlined or optimized away (no entry point); and reliable stack traces when symbols are stripped or frame pointers/debug info are missing (you get addresses, not names, or truncated stacks).
Q: Does eBPF replace application metrics, logs, and tracing?
A: No — it complements them. eBPF is the x-ray of system behavior (syscalls, latency, scheduling) with no code change or restart, but it doesn't know your business meaning ("checkout step 3 for customer X"). Use both: each answers questions the other can't.
👩‍🏫 I'm your teacher — ask me anything

Production safety is where the "but what if…" questions get good. Ask me things like: "Walk me through rewriting one of my per-event one-liners into an in-kernel aggregate", "How would I run a tracing agent on my Kubernetes nodes as a DaemonSet?", or "My kernel has no /sys/kernel/btf/vmlinux — what am I limited to?" I'll tailor it to your stack.