eBPF for system tracing
eBPF Use Cases
William  

Trace System Calls with eBPF

I show how I trace system calls using eBPF to get practical, low-overhead visibility into application behavior.

I run small sandboxed programs inside the linux kernel to capture execve, connect, send and recv events. The verifier and JIT keep the approach safe and fast. I explain what you need: a compatible kernel, basic permissions, and one of the standard toolchains to load probes.

My goal is hands-on: start on a single host, then expand to Kubernetes with tools that scale. I prefer bpftrace and bcc for quick checks, and Inspektor Gadget as a DaemonSet to follow requests across pods.

Throughout I focus on low overhead, clear outputs, and exportable data so you can move from raw events to decisions. If you want deeper performance profiling, see this practical guide on kernel performance with eBPF.

Table of Contents

Key Takeaways

  • I demonstrate tracing steps you can run today on a compatible host.
  • Sandboxed programs give kernel-level visibility without heavy agents.
  • Use bpftrace/bcc for host checks and Inspektor Gadget in clusters.
  • Focus on execve, connect, send, recv to expose real app behavior.
  • The kernel verifier and JIT protect stability and keep overhead low.

Why trace system calls with eBPF instead of legacy agents

I gather syscall events directly where they occur to avoid polling and reduce noise. That means I run code only on events—no continuous sampling—and the CPU and memory impact stays low.

The verifier checks safety before load. It blocks unsafe memory access and endless loops. That makes the approach safer than many kernel modules, which can crash a host if buggy.

  • Low overhead: event-driven hooks cut CPU and memory use versus user-space daemons.
  • Higher fidelity: syscall-level capture improves latency, error codes, and path attribution for applications and containers.
  • Broader visibility: all languages and runtimes hit the kernel—no SDKs or per-service work.
  • Reduced monitoring-induced issues: fewer IO or cache storms under peak load.
  • Trade-offs: kernel versions, distro backports, and managed-platform policies affect available hooks; plan a fallback when needed.

I use this approach when I need tight performance monitoring and reliable network and process visibility with minimal operational risk.

What you need before you start

Start by confirming that the host kernel supports modern helpers and BTF. I check versions first: Linux 4.14+ is the minimum. I prefer 5.10+ for helpers, ring buffer, and a stronger verifier.

Permissions: loading programs typically needs root or CAP_BPF / CAP_SYS_ADMIN. Unprivileged modes may be disabled on many distros. Treat unprivileged access as a controlled lab feature, not a production default.

  • Confirm exact kernel build and distro backports—Red Hat, Ubuntu, and Debian vary.
  • Validate headers and BTF: CO-RE with BTF reduces per-kernel compilation pain and eases working with source code.
  • Keep /sys/kernel/tracing and debugfs available for quick checks and printk debugging.

Tools: pick fast. I use bpftrace for ad-hoc probes and bcc for single-host scripts. For production loaders I rely on libbpf-based code. In clusters I deploy Inspektor Gadget as a DaemonSet.

Plan data paths: ring buffer for streaming events to user space; maps for counters and state; file or socket sinks for export. Lock down who can load programs and audit access. I keep a minimal libbpf skeleton and a small test application to validate hooks before I point this at live application traffic.

How eBPF tracing works in the Linux kernel

I keep this practical: understand the safety gate, pick the right hook, and keep code small.

I rely on two kernel stages when I load any probe. The verifier inspects every path to prove the code terminates and that pointers stay inside bounds. It rejects unsafe patterns before anything runs.

After verification the JIT compiles verified bytecode to native instructions for my CPU. That turns an ebpf program into fast handlers that run in kernel space with low overhead.

Which hooks matter

  • Tracepoints: use sys_enter/sys_exit tracepoints for broad system calls coverage. Example: SEC(“tp/syscalls/sys_enter_execve”).
  • Kprobes / kretprobes: attach when you need specific kernel function details.
  • Uprobes: instrument user-space library or binary calls without kernel patching.
  • Generic tracepoints: handy for stable, well-known events.

I keep each program tiny: no unbounded loops, simple maps, and minimal per-event work. I store counts and state in maps and push events via a ring buffer to user space. Tag events with process and container metadata so I can attribute costs cleanly.

Quick start: trace syscall activity on a single host

Get a live view of calls and errors with a few short commands on Ubuntu.

I confirm the kernel first: uname -r and check >= 4.14. I prefer 5.10+ for extra helpers and ring buffer support.

Install a starter tool: sudo apt install bpfcc-tools. That gives ready scripts and tracing tools like execsnoop, opensnoop, and killsnoop-bpfcc.

Run a quick example: sudo killsnoop-bpfcc. It shows which process sent kill calls — fast validation of unexpected terminations.

Watch live logs in another shell: sudo cat /sys/kernel/tracing/trace_pipe. bpf_printk output appears there when I prototype.

  • Filter noise: add a PID or process name to the command line.
  • Capture entry and exit to get args and result codes; that helps find why a call failed.
  • Check impact: monitor top or perf stat while the probe runs to keep overhead low.
  • Save outputs to a file so you can compare runs and refine what fields you export as data.

Do not enable unprivileged modes on shared hosts. I use sudo to load probes and keep audit logs of who ran the tool. Validate the path end-to-end by running a small application that execs a known binary and confirming the example output.

eBPF for system tracing: step-by-step walk-through

I load a tiny handler that emits a compact event to validate behavior quickly.

Attach to execve and other syscall tracepoints

I attach an ebpf program at SEC(“tp/syscalls/sys_enter_execve”) to catch process starts and args. I add the exit tracepoint to capture return codes.

Keep the kernel code minimal: fixed-size structs, no loops, bounded userspace reads. Use a loader that opens the object, finds the program, and attaches it.

A modern workspace showcasing an eBPF program in action, with a clean desk featuring a high-resolution monitor displaying terminal windows filled with colorful code snippets and system traces. In the foreground, capture a close-up of a network diagram pinned on a corkboard, illustrating the flow of data and system calls. The middle layer should include a professional software engineer, dressed in smart casual attire, intently analyzing the screen, with subtle reflections of code on their glasses. The background features a sleek workstation with ambient lighting, soft blue tones creating a tech-savvy atmosphere, and additional monitors displaying system performance metrics. The overall mood is professional and focused, emphasizing innovation and deep technical engagement in system tracing using eBPF.

Read events in user space with maps and ring buffers

I collect timestamp, PID/TID, comm, and selected args. The kernel handler packs the struct and calls bpf_ringbuf_submit.

On the user side I call ring_buffer__new and run ring_buffer__consume in a short loop. Sleep briefly between polls to cap CPU.

Filter by PID, process name, or container ID

I keep filters in an in-kernel map. The program checks the map and drops unwanted events before submitting.

FilterMap TypeImpact
PIDhashZero extra events; low CPU
Process nameperf_event arrayEasy updates; small memory
Container IDcgroup id mapGood attribution; slightly more checks
  • Expand to sys_enter_connect, sys_enter_sendto, and sys_enter_recvfrom to link network calls to process activity.
  • Correlate events in user space by PID/TID and timestamp—keep kernel code focused on emit-only work.
  • Measure overhead: run high-rate tests and drop fields or add sampling if ring buffer drops appear.

Kubernetes scenario: trace a live app with Inspektor Gadget

I deploy Inspektor Gadget across the cluster to capture live request flows between pods. This gives process, pod, and network context without installing agents in each container. Minimum node kernel 5.10+ is recommended for best helper support.

  • Install plugin: kubectl krew install gadget. Confirm with kubectl gadget version.
  • Deploy DaemonSet: kubectl gadget deploy — it creates gadget pods in the gadget namespace.
  • Verify readiness: kubectl get pods -n gadget should show one Running pod per node. If not, check node kernel and permissions.

Follow calls across pods

Use traceloop to watch request flows: kubectl gadget traceloop -n demo-app. It shows pod, container, and PID context so I can link an application call to the replica that handled it.

Trace TCP and generate traffic

Run TCP traces with kubectl gadget trace_tcp -n demo-app to see source/destination pairs, ports, and timing. Narrow scope by pod name or label to reduce noise.

Practical checks: curl the frontend, port-forward if needed, and confirm connect, send, and recv patterns appear. Save outputs to files per test so you can compare data across deployments and kernel versions.

From data to insight: exporting and interpreting traces

I turn runtime events into searchable data that links processes, pods, and errors.

I export JSON lines per event. Each line has a timestamp, PID/TID, pod and container IDs, syscall name, args, duration, and result code.

I enrich records at the edge: node name, namespace, and pod labels. That gives offline tools the context they need to join data to services and application owners.

I stitch related calls by PID and time to form request timelines. Then I compute p95 and p99 latencies by container and call type to surface slow pods.

FieldPurposeStorageExample
timestampOrder events; compute latencyJSONL / ClickHouse2025-06-01T12:34:56.789Z
pid_tidCorrelate spans and processesElasticsearch / SQLite1234:1234
pod/containerAttribute to application and ownerLong-term aggregatesdemo-app / abcdef
syscall, args, rcDiagnose failures and hotspotsRaw archive + daily rollupsconnect, ip:port, -110

I flag common failures by errno and link each to pod and source IP. That speeds root-cause analysis and supports rollout checks.

I sample low-value events when volume spikes. I keep raw exports for forensics and store daily aggregates for trend analysis.

Finally, I feed exports into ClickHouse, Elasticsearch, or a simple SQLite workflow. That gives fast queries and actionable analysis to drive operational decisions.

Moving beyond printf: maps, ring buffers, and BTF

I prefer emitting compact events into a shared queue rather than printing in-kernel. That shift gives predictable delivery under bursty load and keeps kernel handlers tiny.

Why bpf_ringbuf beats perf buffer on multi-CPU workloads

The ring buffer is a global MPSC queue. It avoids per-CPU tuning and the drop spikes you see when one CPU dominates events.

In the kernel I reserve space with bpf_ringbuf_reserve, write the small struct, then call bpf_ringbuf_submit. On the user side ring_buffer__new creates the consumer and ring_buffer__consume reads events. That pattern keeps memory pressure low and throughput high.

Use BTF and CO-RE to cut build friction

BTF carries type info that lets CO-RE relocate fields across kernels. I compile once and run the same object on many kernels without brittle source code patches.

That reduces loader complexity and keeps the program portable. I lean on libbpf so my loader code stays small and robust when helpers change.

  • I keep event structs minimal: include PID, timestamp, and key args — push heavy formatting to user space.
  • I pick maps by access: array and hash for filters, LRU for churn, per-CPU maps for counters if needed.
  • Validate buffer health: track drops, tune ring size, and test CPU affinity under bursts.

Practical rule: keep kernel space handlers tight and push enrichment into the consumer. That yields reliable data with low overhead and easier code maintenance.

Performance and safety considerations in production

Keep production probes tight: attach only where traffic matters and fail fast if load rises.

Minimize overhead

I attach only to the call paths I need. Fewer hooks reduce cpu and memory use during spikes.

I keep handlers tiny. Pack a small struct, avoid parsing, and send raw events to user space for formatting.

I sample high-volume, low-value paths. I keep full fidelity for errors and outliers.

Security posture

I restrict who can load programs. Limit CAP_BPF and CAP_SYS_ADMIN to a small ops group and audit every load and unload.

I keep unprivileged access off in production. Many distros disable it by default—keep it that way unless you have a secure sandbox.

I protect exported data: encrypt at rest and in transit and limit reads to authorized teams.

  • Monitor lost events and ring buffer backpressure.
  • Stage kernel upgrades and validate helpers before rollout.
  • Have an immediate kill switch: feature flags or detach commands to restore headroom.
ControlWhy it mattersAction
Selective attachLimits CPU and memory useAttach minimal hooks; filter in-kernel
Tiny handlersReduces kernel work and risksEmit compact events; format in user space
Access controlReduces blast radius of buggy loadsRestrict caps; log load/unload

Final note: the verifier and JIT protect safety and efficiency. Kernel modules may be faster but lack those guards and can crash the kernel if buggy—I use them only after careful review.

When to use eBPF vs kernel modules for tracing

I pick an approach by mapping concrete constraints: latency, safety, and maintenance. I state goals first—what visibility I need, and how much risk the fleet can take.

Short verdict: I use ebpf by default. The verifier and JIT give strong safety and wide distro support. That makes iterative debugging and deployment faster for most teams.

Trade-offs: safety, latency, and maintenance in real systems

Kernel modules run with full kernel privileges. They can be lean and slightly faster because they avoid BPF ABI calls. That can help extreme low-latency needs when you fully control the fleet.

Modules demand heavy review and upkeep. One bad pointer can crash a host. I accept that only when performance gains justify the maintenance cost.

  • I favour portability: ebpf with BTF and CO-RE runs across many linux kernel builds without code forks.
  • I choose modules when latency is the overriding metric and I control all nodes.
  • I document the way we choose: clear criteria—latency, safety, and fleet control—so future developers can repeat the decision.
Aspectebpfkernel modules
SafetyVerifier checks; limited kernel surfaceFull privilege; higher crash risk
PerformanceJIT is fast enough for most observability tasksLower overhead in tight, controlled cases
MaintenancePortable; easier to iterate and shipMore testing and review; version-sensitive

I keep an exit strategy. If I must switch approaches I run parallel pipelines and migrate without downtime. That protects production while we compare performance and operational costs.

Where eBPF tracing shines in real environments

I observe app behavior and network flows without changing application code. That gives clear timelines when incidents happen. Big teams at places like Facebook and Netflix used this early; modern tools — Inspektor Gadget, Pixie, Hubble, Tetragon — add cluster context today.

I use it to debug flaky microservices: I follow connect, send, and recv patterns between pods to find failing dependencies fast. I reduce MTTR at 3 a.m.: node-level traces explain what happened before a pod vanished. I profile startup paths and spot slow execve or file open calls that delay deploys.

I also watch noisy neighbors and baseline performance per service and node. I flag suspicious process spawns and odd network packets without injecting libraries into apps. I export clean events to existing stores and correlate logs, traces, and metrics. See how I monitor packet drops with monitor packet drops as a practical example.

FAQ

What does "Trace System Calls with eBPF" let me do?

It lets you observe kernel-level calls from live processes with minimal overhead. You can capture execve, open, read, write, and socket activity — then export that data to userspace for filtering, aggregation, or alerting. This gives visibility into application behavior without loading fragile kernel modules.

Why choose this approach over legacy agents or kernel modules?

The modern extended Berkeley Packet Filter mechanism runs verified programs inside the kernel safely. That means lower risk, faster development, and less maintenance than out-of-tree modules. You get precise hooks and dynamic attach points — no reboot, no recompiling the kernel, and smaller performance impact when you scope probes correctly.

What kernel and distro support do I need to get started?

Use kernels 4.14 and later; 5.10+ is preferable for richer features and stability. Most mainstream distributions provide the needed headers and BPF tooling in recent releases. Check your distro packaging for libbpf, bpftrace, or bcc if you want prebuilt tools.

What permissions are required to run tracing tools?

Root or sudo is the default path because attaching programs into the kernel needs elevated access. Some setups allow controlled unprivileged loading via kernel flags and seccomp profiles — but production setups should limit that surface and audit who can load programs.

Which tools should I use: bcc, bpftrace, libbpf-based programs, or Inspektor Gadget?

Use bcc for quick Python-driven scripts, bpftrace for terse one-liners, and libbpf for production-grade C programs. Inspektor Gadget is convenient in Kubernetes — it packages probes as a DaemonSet so you can trace pods without manual host changes. Pick based on speed of iteration versus long-term robustness.

How does tracing work inside the kernel — is it safe?

The kernel verifier checks programs before load, ensuring memory safety and bounded loops. The JIT compiles permitted programs for speed. Combined, they let you run small, predictable code paths in kernel space without risking common kernel bugs from hand-written modules.

What program types and hooks are available?

You can attach to syscall tracepoints, kprobes (kernel functions), tracepoints (stable kernel events), and uprobes (user-level functions). Each hook fits different needs: tracepoints for stable metrics, kprobes for deep introspection, and uprobes for tracing specific binaries.

How do I trace activity on a single host quickly?

Start with bpftrace or a bcc script: attach to execve or relevant syscalls, filter by PID or process name, and stream events to stdout or a ring buffer. Keep the probe minimal — capture only needed fields — then extend to maps or ring buffers when you need higher throughput.

How do I attach to execve and other syscall tracepoints safely?

Write a short probe that collects essential fields (timestamp, pid, comm, args) and push them into a per-CPU map or ring buffer. Let the verifier validate the program. Test on a dev node, verify low overhead, then roll to staging with sampling or selective PID filters to limit load.

How do I read events from the kernel into userspace?

Use maps or bpf_ringbuf to transfer structured events. Userspace libraries (libbpf, bcc) provide APIs to poll and consume those buffers. For multi-CPU workloads, ring buffers reduce contention and increase throughput compared with older perf buffers.

How can I filter traces by PID, process name, or container ID?

Apply filters inside the probe: check current->pid, compare comm for process name, or read cgroup id/container metadata available via helpers or BTF. Doing filtering in-kernel avoids expensive userspace post-filtering and keeps overhead low.

How do I trace a live Kubernetes app with Inspektor Gadget?

Deploy the gadget DaemonSet to your cluster nodes, wait until pods report Ready, then run gadget commands (like traceloop or tcp tracing) against a target pod. The tool handles attaching probes into container namespaces so you can follow requests across pods without manual host changes.

How do I follow network requests across pods and containers?

Combine socket syscall probes or TCP tracepoints with PID and network tuple correlation. Capture timestamps, source/destination, and request identifiers, then stitch events in userspace by connection or trace id. Tools like Inspektor Gadget simplify the cross-pod attachment.

How do I get actionable insight from raw trace data?

Export events to a processing pipeline: aggregate by endpoint, compute latencies, and highlight anomalies. Use maps to keep counters or histograms in-kernel for efficient summarization and periodically flush to a metrics backend for dashboards and alerts.

When should I move beyond printf-style probes to maps, ring buffers, and BTF?

Move when you need higher throughput, lower overhead, or richer type safety. Maps and bpf_ringbuf scale better on multi-CPU systems. BTF (BPF Type Format) simplifies development and lets you rely on kernel types, reducing maintenance headaches.

Why prefer bpf_ringbuf over perf buffer for multi-CPU workloads?

Ring buffers reduce per-CPU contention and avoid costly copy paths that perf buffer uses. They provide lower latency and higher throughput for bursty event streams, which matters when tracing production services under load.

What performance and safety limits should I enforce in production?

Keep probes small and event-driven. Attach selectively and use sampling for high-volume paths. Monitor CPU and memory; enforce rate limits and drop policies in userspace consumers. Lock down who can load programs and encrypt or restrict access to trace outputs.

When is a kernel module still the right choice?

Consider modules when you need uninterrupted, heavy-weight functionality that eBPF cannot provide — for example, persistent hooks that require privileged state or complex long-running transactions not suited to verifier constraints. For most observability and lightweight control tasks, the verified approach wins.

In what environments does this tracing approach shine?

It excels in cloud-native hosts, high-density multi-tenant servers, and distributed apps where you need low-impact visibility. It helps root-cause performance issues, follow request flows, and secure workloads with minimal operational friction.

Related: Pixie eBPF: Auto-Instrument Kubernetes Without Code Changes

Related: BCC eBPF Compiler for Linux Kernel Tracing

Related: Operationalizing Falco: eBPF Driver Setup and Alert Routing

Related: Linux Syscall Tracing: Find Latency in 3 Steps

Related: Kernel Overhead: What It Is and How eBPF Cuts It