Linux Performance Series · Part 8

bpftrace / eBPF Tools

Article 8 of 9

Jun 5, 2026 · devops · 32 min read · 6400 words advanced

bpftrace — the one-liner language for tracing anything.

devops linux performance bpftrace ebpf

TL;DR — BCC (Part 7) gives you pre-made tools; bpftrace gives you a tiny awk-like language to write your own eBPF tracing in one line. When no ready tool fits your exact question, bpftrace answers it in seconds. This chapter teaches the language — probe syntax, filters, actions, built-in variables, and the map/aggregation functions that do in-kernel summarisation (count(), hist(), @ maps) — then walks the shipped tools on the diagram (opensnoop, biolatency, runqlat, tcpretrans, tcpdrop, oomkill, and more). By the end you can read and write bpftrace, not just run it.

What bpftrace is, and why it's different

BCC tools are excellent, but each is a fixed program answering a fixed question. Real investigations throw odd questions: "histogram the size argument to this specific function, but only for process X." No pre-made tool covers that. bpftrace is the answer — a high-level language, inspired by awk and DTrace, that compiles a one-line (or short multi-line) script into eBPF, loads it, aggregates in-kernel, and prints the result. You go from question to data in one command.

It sits one rung above BCC in convenience and one rung below in maximum programmability: for the vast majority of ad-hoc tracing, bpftrace is the right tool, and it's the one whose syntax shows up throughout this blog's eBPF examples. Learning its handful of concepts pays off forever, because they're the same concepts (probes, actions, maps) the whole eBPF ecosystem is built on.

Brendan Gregg — bpftrace/eBPF Tools diagram

The original — Brendan Gregg's "bpftrace/eBPF Tools" (github.com/bpftrace/bpftrace, early 2019). The simplified diagram below walks it box by box.

Applications / Runtimes opensnoop · statsnoop · bashreadline · gethostlatency syscall — syscount · execsnoop · killsnoop VFS — vfsstat xfsdist TCP — tcpconnect tcpretrans · tcpdrop Sched — runqlat VM — oomkill Disks — biosnoop · biolatency bpftrace — write any probe

Fig 1 — bpftrace ships per-subsystem tools too, but its real power is the language on the left: write a probe for any layer in one line.

Anatomy of a bpftrace program

Every bpftrace program is one or more blocks of the form:

probe /filter/ { action }
  • probe — what event to hook (a kprobe, tracepoint, uprobe, the timer, etc.).
  • filter (optional, in / /) — a condition; the action only runs if true.
  • action — what to do when it fires: print, record into a map, compute.

The classic first program:

# count syscalls by process name, print on Ctrl-C
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

That single line: hook the syscall-entry tracepoint; for each event, increment a map keyed by the process name (comm). On exit, bpftrace prints the map automatically. No file, no compile — that's the whole appeal.

Probe types

ProbeFires on
kprobe:func / kretprobe:funckernel function entry / return
tracepoint:cat:eventa stable kernel tracepoint (preferred — survives upgrades)
uprobe:/bin/x:func / uretprobeuser-space function entry / return
usdt:/bin/x:probestatically-defined app trace point
profile:hz:99 / interval:s:1timer — sampling / periodic output
software: / hardware:software events / PMC hardware events
BEGIN / ENDprogram start / end (print headers, dump maps)

Built-in variables & functions

Built-inMeaning
pid, tid, commprocess id, thread id, process name
nsecstimestamp in nanoseconds (for measuring durations)
arg0..argNprobe arguments; retval for return values
kstack / ustackkernel / user stack trace
count(), sum(), avg(), min(), max()aggregations into maps
hist() / lhist()power-of-2 / linear histograms — the killer feature
@name[keys]a map — in-kernel key/value store for aggregation
printf()formatted per-event output
The @ map plus hist() is what makes bpftrace powerful and cheap: aggregation happens in the kernel, so you ship a small histogram to user space instead of one event per occurrence. That's how you trace a hot path at low overhead — the same mechanism behind biolatency's histogram in our database post.

One-liners that earn their keep

The fastest way to learn bpftrace is to read its famous one-liners. Each answers a real question:

# files opened, with process + filename
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'

# block I/O latency as a histogram (the biolatency core)
bpftrace -e 'kprobe:blk_account_io_start { @s[arg0]=nsecs }
  kprobe:blk_account_io_done /@s[arg0]/ { @us=hist((nsecs-@s[arg0])/1000); delete(@s[arg0]); }'

# read sizes requested, as a histogram, for one process
bpftrace -e 'kprobe:vfs_read /comm=="postgres"/ { @ = hist(arg2); }'

# count kernel stacks leading to TCP retransmits (who/why)
bpftrace -e 'kprobe:tcp_retransmit_skb { @[ustack, comm] = count(); }'

# scheduler run-queue latency histogram
bpftrace -e 'tracepoint:sched:sched_wakeup { @qt[args->pid]=nsecs }
  tracepoint:sched:sched_switch /@qt[args->next_pid]/ {
    @us=hist((nsecs-@qt[args->next_pid])/1000); delete(@qt[args->next_pid]); }'

Notice the pattern in the latency ones: timestamp on the start probe into a map keyed by an id, compute the delta on the matching end probe, drop into a hist(). That entry/return timing idiom is 80% of custom performance tracing — once it clicks, you can measure the latency of almost any kernel or user function.

The shipped tools

Like BCC, bpftrace ships ready-made tools (the .bt scripts) pinned across the diagram. They're the same investigations as Part 7, re-implemented as readable bpftrace you can crack open and modify.

LayerTools
Apps / syscallopensnoop, statsnoop, syncsnoop, execsnoop, killsnoop, syscount, bashreadline, gethostlatency
VFS / FSvfscount, vfsstat, xfsdist, writeback, mdflush
Blockbiosnoop, biolatency, bitesize
Networktcpconnect, tcpaccept, tcpretrans, tcpdrop
Scheduler / CPUrunqlat, runqlen, cpuwalk, offcputime, pidpersec
Memoryoomkill
Othercapable

Two worth singling out. tcpdrop traces packets the kernel drops (not just retransmits) with the stack that dropped them — it answers "why is the kernel discarding these packets?", a level deeper than tcpretrans. cpuwalk samples which CPU a process runs on over time, exposing whether a workload is bouncing across cores/NUMA nodes (a reason to pin it, per Part 4). Because each ships as a readable .bt file, the tool is the tutorial — cat $(which biolatency.bt) to see exactly how it works and tweak it.

bpftrace /usr/sbin/biolatency.bt     # run a shipped tool
cat /usr/share/bpftrace/tools/tcpdrop.bt   # read it to learn
bpftrace -l 'tracepoint:tcp:*'       # list available probes

bpftrace vs BCC — when to use which

bpftraceBCC
Best forad-hoc one-liners, custom questions, quick scriptscomplex, polished, repeatable tools
Languageconcise awk-like DSLPython/C — full programming
Speed to answerseconds (one command)more setup, more control
Custom UIs / argslimitedfull (argparse, formatting)

The practical rule from Brendan Gregg himself: reach for a bpftrace one-liner for a quick custom question; promote it to a BCC tool only if you need a robust, reusable, well-argument-parsed program. In day-to-day work that means bpftrace covers most investigations, and the BCC toolbox (Part 7) covers the standard ones. When a BCC tool fails to attach a kprobe on a newer kernel, the bpftrace tracepoint version is the reliable fallback (we hit exactly this in the database post).

Safety & overhead

Same eBPF guarantees as the rest of the stack: the verifier rejects anything that could crash or hang the kernel, so a broken script errors out rather than panicking the box. Overhead is low when you aggregate in-kernel (@ = hist(...), count()) and higher when you printf per event on a hot probe. Bound chatty scripts with a filter (/comm=="postgres"/), a duration, or a PID. Production-safe for the aggregating tools; targeted windows for the per-event ones.

Hands-on: write your own tracer in one line

The fastest way to learn bpftrace is to run its one-liners and read the output. Each below answers a real question — command, output, decision. All need root.

Which process makes the most syscalls?

sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
@[postgres]: 184203
@[node]: 990210          <- node dominates syscalls

Read it: node is making 5× the syscalls of the DB. Do this: drill into which syscall with the next one-liner; a flood usually means chatty I/O or polling.

Histogram a function's latency (the timing idiom)

# vfs_read latency, in microseconds, as a histogram
sudo bpftrace -e 'kprobe:vfs_read { @s[tid]=nsecs }
  kretprobe:vfs_read /@s[tid]/ { @us=hist((nsecs-@s[tid])/1000); delete(@s[tid]); }'
@us:
[8, 16)    9203 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[16K, 32K)  140 |@                          |   <- 16-32 ms tail

Read it: timestamp-on-entry → delta-on-return → hist() is 80% of custom tracing. Here reads are mostly fast with a small ms-scale tail. Do this: add a /comm=="postgres"/ filter to scope it to the DB.

What's behind the TCP retransmits?

sudo bpftrace -e 'kprobe:tcp_retransmit_skb { @[comm] = count(); }'
@[postgres]: 47        <- retransmits on DB traffic

Do this: non-zero and climbing on a DB process = packet loss on replication/query sockets; check the NIC and link (Part 2/4).

Read sizes a process requests

sudo bpftrace -e 'kprobe:vfs_read /comm=="postgres"/ { @ = hist(arg2); }'
@:
[8K, 16K)   8200 |@@@@@@@@@@@@@@@@@@@@@@@@@@|   <- mostly 8K page reads
[256K, 512K)  12 |                          |

Read it: Postgres reading in 8K pages — expected. A surprise of tiny reads would hint at a bad access pattern or missing index.

Run a shipped tool, then read it to learn

sudo bpftrace /usr/sbin/biolatency.bt     # run it
cat /usr/share/bpftrace/tools/tcpdrop.bt  # learn how it works
sudo bpftrace -l 'tracepoint:tcp:*'       # list probes you can hook

Do this: the shipped .bt tools are readable source — copy one, tweak the filter or the key, and you've written a custom tool in minutes. tcpdrop goes deeper than tcpretrans: it shows packets the kernel drops with the stack that dropped them.

Takeaways

  • bpftrace = write your own tool in one line. When no pre-made tool fits, the DSL answers the exact question in seconds.
  • Three concepts run everything: probes (what to hook), filters (when), actions (what to do) — plus @ maps + hist() for cheap in-kernel aggregation.
  • The timing idiom — timestamp on entry into a keyed map, delta on return into a hist() — measures the latency of almost any function.
  • Shipped .bt tools are readable. cat one to learn; tcpdrop and cpuwalk go deeper than their BCC cousins.
  • bpftrace for ad-hoc, BCC for polished. And bpftrace's tracepoint tools are the fallback when BCC kprobes won't attach.

References

Extra reads

Built from Brendan Gregg's "bpftrace/eBPF Tools" diagram (github.com/bpftrace/bpftrace, 2019). Syntax shown targets recent bpftrace; argument access (args->field) and some builtins vary by version. Aggregate in-kernel for low overhead; bound per-event scripts by filter/duration.

← prev: Part 7 — bcc/BPF Tools next: Part 9 — BPF Performance Tools →
© cvam — written in plaintext, served warm