Trace System Calls with bpftrace

Khimananda Oli 8 min read Virtualization
Trace System Calls with bpftrace

By Khimananda Oli | Last reviewed: August 2026

When standard logs and metrics fail to explain intermittent latency or silent failures, you must trace system calls with bpftrace to observe kernel-user interactions directly. Unlike legacy strace methods that impose massive overhead, eBPF-based tracing offers safe, programmable visibility into production Linux systems without restarting services. This guide provides battle-tested scripts and safety patterns derived from years of debugging high-traffic infrastructure.

How do you safely trace system calls with bpftrace in production?

Production environments demand caution. While it is powerful, an unbounded trace can saturate CPU or fill buffers faster than userspace can consume them. When I audit infrastructure for Ubuntu server security best practices, I treat dynamic tracing as a privileged operation requiring strict scoping.

Verify kernel support and permissions

Before writing a single line of code, confirm your kernel supports BPF Type Format (BTF). Modern distributions like Ubuntu 24.04+ and RHEL 9+ ship this by default. BTF allows bpftrace to understand kernel structures without manual header parsing, making scripts portable across kernel versions.

# Check for BTF support
ls /sys/kernel/btf/vmlinux

# Verify bpftrace version (aim for 0.21+ in 2026)
bpftrace --version

# Ensure CAP_BPF and CAP_PERFMON capabilities
sudo setcap cap_bpf,cap_perfmon+ep /usr/bin/bpftrace

Apply mandatory safety guardrails

Never run unbounded traces on high-frequency syscalls like read, write, or futex without filters. The perf ring buffer is finite; overflowing it causes lost events and potential backpressure on the kernel. In my experience managing SOC 2 compliant environments, we enforce these rules via policy-as-code:

  • PID Filtering: Always scope to a specific process ID or cgroup during initial investigation.
  • Rate Limiting: Use nsecs deltas or map-based throttling to cap output to <1000 lines/sec.
  • Timeout Enforcement: Wrap commands in timeout 30s bpftrace ... to prevent accidental infinite runs.
  • Tracepoint Preference: Prefer tracepoint:syscalls:* over kprobe:*. Tracepoints are stable ABI; kprobes can crash if internal struct layouts change between kernel updates.
Kernel SpaceSyscall Entry/ExitTracepoints / KprobesBPF Program (Sandboxed)Perf Ring BufferLockless FIFO Queue⚠ Overflow = Lost EventsBackpressure ProtectionUserspace Consumerbpftrace CLI / Custom ToolAggregation & FilteringSafe Output RateSafety Guardrails (Enforced)PID/Cgroup FilterRate LimitingTimeout EnforcementTracepoint > KprobePrevents CPU saturation, buffer overflow, and kernel instabilityRequired for SOC 2 / ISO 27001 audit compliance
Safe architecture for tracing system calls with bpftrace: kernel probes feed a bounded ring buffer, consumed by rate-limited userspace tools under strict guardrails.

What are the most useful bpftrace one-liners for debugging syscalls?

You rarely need to write complex programs from scratch. Most production issues map to known patterns. These one-liners have solved more incidents in my career than any dashboard. They work on any modern Linux distro with BTF support.

Identify processes causing file open errors

Silent failures often manifest as ENOENT or EACCES. This script catches every failed openat call, printing the process name, PID, filename, and error code. It’s invaluable when debugging permission issues in containerized apps where file permissions get tangled across namespaces.

sudo bpftrace -e '
tracepoint:syscalls:sys_exit_openat
/args->ret < 0/ {
    printf("%-6d %-16s %s (err=%d)\n",
           pid, comm, str(args->filename), args->ret);
}'

Measure syscall latency distributions

Averages lie. When debugging storage latency, you need percentiles. This script uses a histogram map to show the distribution of fsync durations. If your p99 spikes while p50 stays flat, you have a tail-latency problem that aggregate metrics hide.

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_fsync { @start[pid] = nsecs; }
tracepoint:syscalls:sys_exit_fsync
/@start[pid]/ {
    @duration_us = hist((nsecs - @start[pid]) / 1000);
    delete(@start[pid]);
}'

Track network socket creation by process

Security audits frequently require proving which binaries initiate outbound connections. This traces socket syscalls, filtering for TCP (type=1) and IPv4/IPv6 families. Pair this with network policies validation to detect policy violations before attackers exploit them.

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_socket
/args->family == AF_INET || args->family == AF_INET6/ {
    printf("%-6d %-16s socket(family=%d, type=%d)\n",
           pid, comm, args->family, args->type);
}'

How does bpftrace compare to strace and perf for syscall tracing?

Choosing the wrong tool wastes hours. Each has a distinct niche based on overhead, safety, and data granularity. Understanding these trade-offs prevents production incidents caused by observer effects.

Featurebpftracestraceperf trace
OverheadLow (<5% typically)Very High (100x+ slowdown)Moderate (10-30%)
SafetySandboxed verifierPtrace stops processSafe but limited filtering
ProgrammabilityFull C-like language + mapsNone (raw stream only)Limited event selection
AggregationIn-kernel histograms/countsPost-process requiredBasic stats only
Best ForProduction debugging, custom metricsDev environment reproSampling hot paths
Risk LevelLow (with guardrails)Critical (never in prod)Medium

In practice, I use strace only on isolated dev boxes to reproduce bugs. For any production system, especially those handling customer data under compliance frameworks, bpftrace is the default. Its in-kernel aggregation means you extract insights without shipping terabytes of raw events to userspace—a critical distinction when comparing observability signals at scale.

Tool Comparison: Overhead vs SafetyOverhead →Safety →bpftraceLow OverheadHigh SafetystraceExtreme OverheadUnsafe in Prodperf traceModerate OverheadLimited FlexibilityProduction Sweet Spotbpftrace enables safe, aggregatedsyscall tracing without observer effect
bpftrace occupies the production-safe zone: low overhead combined with kernel-enforced safety, unlike strace which risks service degradation.

How do you build custom bpftrace scripts for advanced syscall analysis?

One-liners solve known problems. Custom scripts solve yours. When investigating complex issues like database connection pool exhaustion or microservice timeout cascades, you need multi-probe correlation and state tracking.

Correlate connect() and close() for leak detection

Socket leaks manifest as growing file descriptor counts. This script tracks open sockets per process and flags those held longer than 60 seconds—a common sign of missing cleanup in error paths. I’ve used similar logic to debug Node.js services where Node.js installations lacked proper signal handlers.

#!/usr/bin/env bpftrace

tracepoint:syscalls:sys_enter_connect {
    @sock_start[pid, args->fd] = nsecs;
}

tracepoint:syscalls:sys_exit_close
/@sock_start[pid, args->fd]/ {
    $dur_ms = (nsecs - @sock_start[pid, args->fd]) / 1000000;
    if ($dur_ms > 60000) {
        printf("LEAK: pid=%d fd=%d held %d ms\n",
               pid, args->fd, $dur_ms);
    }
    delete(@sock_start[pid, args->fd]);
}

interval:s:10 {
    print(@sock_start);
}

Profile syscall patterns by cgroup

In Kubernetes, PID-based filtering breaks due to container restarts. Cgroup-based tracing survives pod rescheduling. This aggregates syscall counts per cgroup, revealing noisy neighbors or runaway containers without modifying pod specs.

sudo bpftrace -e '
tracepoint:raw_syscalls:sys_enter {
    @syscalls[cgroup_path(cgroup)] = count();
}

interval:s:5 {
    print(@syscalls, top=10);
    clear(@syscalls);
}'

Debug DNS resolution latency end-to-end

DNS slowness kills SLOs. This correlates connect to port 53 with subsequent recvfrom latency, giving true resolver time excluding application processing. Combine with DNS configuration guides to validate fixes quantitatively.

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_connect
/args->uservaddr->sin_port == 53/ {
    @dns_start[pid] = nsecs;
}

tracepoint:syscalls:sys_exit_recvfrom
/@dns_start[pid]/ {
    $lat_us = (nsecs - @dns_start[pid]) / 1000;
    @dns_latency_us = hist($lat_us);
    delete(@dns_start[pid]);
}'
Custom Script: Socket Leak Detection Flowsys_enter_connectRecord timestamp@sock_start[pid,fd]Map StorageBPF Hash MapKey: (pid, fd)Value: nsecssys_exit_closeLookup & calc deltaDelete map entryAlert Logicdelta > 60s?Print LEAK warningInterval Probe (Every 10s)Scans @sock_start map for stale entriesCatches leaks where close() never firesEssential for long-running services with async cleanup bugsOutput: Periodic map dump + real-time alerts
Advanced bpftrace pattern: correlating connect/close syscalls with interval-based stale entry detection for comprehensive socket leak diagnosis.

Start Tracing System Calls Safely Today

Tracing system calls with bpftrace transforms opaque kernel behavior into actionable data, but respect its power. Begin with filtered one-liners on non-critical hosts, validate output rates, and graduate to custom scripts only after mastering safety primitives. Your production systems will thank you for the discipline. If you need help designing observability strategies that survive audits and traffic spikes alike, reach out to discuss your infrastructure.

Frequently Asked Questions

Run sudo apt update followed by sudo apt install bpftrace to get the latest stable package from official repositories.

Overhead is typically under two percent for low-frequency syscalls but increases significantly with high-volume events or complex aggregation logic in scripts.

Yes, use the pid filter syntax like syscall:openat /pid == 1234/ to restrict tracing to a single process ID and reduce noise.

Linux kernel 5.8 or newer is recommended for full syscall tracing support, though basic functionality works on 4.18+ with limited features.

bpftrace uses eBPF for safe, low-overhead production tracing while strace attaches via ptrace causing significant slowdowns unsuitable for live systems.

Yes.

Execute bpftrace -l 'tracepoint:syscalls:*' to display all supported syscall entry and exit points on your current kernel version.

Ensure you have CAP_BPF and CAP_PERFMON capabilities or run as root, and verify that kernel.unprivileged_bpf_disabled is not set to 1.

Yes, attach to both entry and exit tracepoints using args->filename and retval respectively, correlating them via thread ID for complete request tracking.

Generally yes with simple probes, but avoid heavy filtering or stack traces on high-throughput databases to prevent latency spikes during peak loads.

Use str(args->filename) comparisons in the predicate block, noting that string matching adds overhead compared to integer-based PID or UID filters.

Limit active probes to fifty or fewer on busy systems to avoid hitting kernel memory limits and degrading scheduler performance during extended tracing sessions.

Not directly; output structured JSON or CSV via printf and pipe to an exporter or log aggregator for metrics visualization in monitoring dashboards.

Verify the tracepoint exists with -l, check dmesg for verifier rejections, and confirm the target process actually invokes that specific syscall.

Scripts can read sensitive memory or arguments, so restrict access via RBAC and audit bpftrace usage to prevent accidental exposure of credentials or PII.