Skip to main content
RunBook Academy

LinuxXLIII · eBPF and Advanced ObservabilityProduction safety

The cost of tracing - eBPF overhead, dropped events and production safety

Expert⏱ ~21 minbpftracebpftool

What you'll learn

  • Rank tracepoints, kprobes and uprobes by cost and explain why the gap is so large
  • Prefer in-kernel aggregation over per-event output, and recognise the symptoms of not doing so
  • Read bpftrace warnings about lost events and map size, and raise the right limit
  • Bound a probe before attaching it, and know which attachments to refuse outright

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-11

Not yet marked complete on this device.

eBPF is often described as having negligible overhead. That is true of a well-chosen probe and false by several orders of magnitude of a badly chosen one. The difference between a one-liner that costs 0.1% and one that halves the throughput of a production database is not the tool - it is which event you attached to and what you did in the handler.

The verifier guarantees a BPF program cannot crash the kernel or loop forever. It guarantees nothing about how often the program runs.

Cost is frequency times per-event work

overhead  =  events per second  x  cost per event

Both factors vary enormously, and the first one varies more.

AttachmentRough per-event costNotes
TracepointTens of nanosecondsStable ABI, compiled-in, cheapest
kprobe on a cold functionTens to low hundreds of nsBreakpoint plus trap handling
kretprobeRoughly double a kprobeNeeds an entry probe plus a return trampoline
uprobeMicrosecondsA trap into the kernel from user space and back
uretprobeWorse than a uprobeSame, twice

The uprobe row is the one that surprises people. A uprobe costs roughly one to two microseconds per hit, because each hit is a full user-to-kernel-to-user round trip. That is fine on a function called a hundred times a second. On a function called a million times a second it is one to two seconds of CPU per second per core - the process simply stops making progress.

Frequency is the part you can look up before attaching:

# How often does this actually fire? Count for 10 seconds first.
sudo timeout 10 bpftrace -e '
  tracepoint:raw_syscalls:sys_enter { @[probe] = count(); }'
@[tracepoint:raw_syscalls:sys_enter]: 4128841

400,000 events per second. At even 50 ns each, that is 2% of one core just to count them - and this is the cheap probe type. Attaching a uprobe at that rate would be catastrophic.

Aggregate in the kernel

The second-biggest cost is what your handler does, and the dominant factor there is whether every event has to reach user space.

# BAD on a busy path: one line of output per event.
sudo bpftrace -e '
  tracepoint:syscalls:sys_enter_openat {
    printf("%s %s\n", comm, str(args.filename));
  }'
# GOOD: aggregate in kernel, transfer a summary at exit.
sudo bpftrace -e '
  tracepoint:syscalls:sys_enter_openat { @[comm] = count(); }'

The first copies a string and pushes a record through the perf ring buffer for every event; the second increments a counter in a kernel map, and the map is read once when the program exits. On a high-rate path that is the difference between a usable tool and a tool that is itself the load.

The aggregating builtins - count(), sum(), avg(), min(), max(), hist(), lhist(), stats() - all do their work in the kernel. Reach for hist() in particular: a latency distribution is almost always more useful than a stream of individual latencies, and it costs a fraction as much.

Bound what you attach

bpftrace expands wildcards into one probe per match, and the count can be startling:

sudo bpftrace -l 'kprobe:vfs_*'
kprobe:vfs_clone_file_range
kprobe:vfs_copy_file_range
kprobe:vfs_create
...

Count before attaching:

sudo bpftrace -l 'kprobe:tcp_*' | wc -l

A pattern like kprobe:* matches tens of thousands of functions. Attaching them takes minutes, holds locks while it does so, and BPFTRACE_MAX_PROBES exists specifically to stop you:

ERROR: Can't attach to 41284 probes because it exceeds the current limit
of 1024 probes.

That error is a guardrail. Raising the variable to get past it, on a production host, is almost always the wrong response.

--dry-run compiles and attaches everything and then exits immediately, which is the cheapest way to find out whether a script will attach cleanly:

sudo bpftrace --dry-run -e 'kprobe:tcp_sendmsg { @ = count(); }'

Bound the run

A tracing session left attached is the failure that outlives the investigation. Bound every one:

# Stop after 30 seconds regardless of what happens to your terminal.
sudo timeout 30 bpftrace -e 'tracepoint:block:block_rq_issue { @ = count(); }'

Or inside the script, which also gives you control over the output:

interval:s:30 { exit(); }

exit() runs the END block and prints the maps, so a bounded run still produces its summary.

Check afterwards that nothing was left behind:

sudo bpftool prog list | tail -20
sudo bpftool map list | tail -20

Programs pinned by a tool that died without cleaning up hold kernel memory and keep running their probes. bpftool prog show gives the load time and process name, which is usually enough to identify an orphan.

Things to refuse

Some attachments are not worth the risk on a production host at all:

AttachmentWhy not
uprobe on malloc, memcpy or similar, host-wideMillions of events per second at microsecond cost
kprobe on _raw_spin_lock or scheduler internalsExtremely hot, and probing the scheduler from the scheduler is its own problem
Any printf per event on a path exceeding a few thousand events per secondRing buffer saturation and lost events
kprobe:* or an unbounded wildcardMinutes of attach time and lock contention
--unsafe anythingIt enables destructive actions - see below

bpftrace --unsafe enables functions such as system(), which runs a shell command from a probe handler. On a probe that fires a thousand times a second, that is a thousand forks a second. It exists for deliberate, scoped automation and has no place in an exploratory session.

A safe default shape

Putting the habits together, an exploratory tracing session on a production host looks like this:

# 1. Does the probe exist on this kernel?
sudo bpftrace -l 'tracepoint:block:block_rq_issue'

# 2. How often does it fire? Short, cheap, bounded.
sudo timeout 5 bpftrace -e 'tracepoint:block:block_rq_issue { @ = count(); }'

# 3. Will the real script attach cleanly?
sudo bpftrace --dry-run /usr/sbin/biolatency.bt

# 4. Run it bounded, aggregating in kernel, with a second session open.
sudo timeout 30 /usr/sbin/biolatency.bt

# 5. Confirm nothing was left attached.
sudo bpftool prog list | tail -5

Five commands, four of which are cheap. The discipline is not paranoia - eBPF genuinely is safe enough to run on production, and that is exactly why it gets run on production, which is why the failure modes matter.

Knowledge check

Knowledge check · 5 questions

  1. Q1. Which attachment type is by far the most expensive per event?

  2. Q2. The BPF verifier guarantees a program cannot crash the kernel, but guarantees nothing about how often the program runs.

  3. Q3. bpftrace prints "Lost 1288 events" during a run. What should you conclude?

  4. Q4. Which habits make an exploratory tracing session on a production host safe? Select all that apply.

  5. Q5. Why must kprobe-based tooling be re-validated after a kernel upgrade?

Passing score: 75%. Answers are checked in this browser.