LinuxXLIII · eBPF and Advanced ObservabilityProduction safety
The cost of tracing - eBPF overhead, dropped events and production safety
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
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.
| Attachment | Rough per-event cost | Notes |
|---|---|---|
| Tracepoint | Tens of nanoseconds | Stable ABI, compiled-in, cheapest |
| kprobe on a cold function | Tens to low hundreds of ns | Breakpoint plus trap handling |
| kretprobe | Roughly double a kprobe | Needs an entry probe plus a return trampoline |
| uprobe | Microseconds | A trap into the kernel from user space and back |
| uretprobe | Worse than a uprobe | Same, 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:
| Attachment | Why not |
|---|---|
uprobe on malloc, memcpy or similar, host-wide | Millions of events per second at microsecond cost |
kprobe on _raw_spin_lock or scheduler internals | Extremely 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 second | Ring buffer saturation and lost events |
kprobe:* or an unbounded wildcard | Minutes of attach time and lock contention |
--unsafe anything | It 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
Q1. Which attachment type is by far the most expensive per event?
Q2. The BPF verifier guarantees a program cannot crash the kernel, but guarantees nothing about how often the program runs.
Q3. bpftrace prints "Lost 1288 events" during a run. What should you conclude?
Q4. Which habits make an exploratory tracing session on a production host safe? Select all that apply.
Q5. Why must kprobe-based tooling be re-validated after a kernel upgrade?
Passing score: 75%. Answers are checked in this browser.