Kernel Version Requirements and Verification

eBPF has been in the kernel since 3.18, but the features you actually want for observability landed progressively. BTF (BPF Type Format) shipped in 5.2 and is the foundation for CO-RE (Compile Once, Run Everywhere) tools. Without BTF you are compiling eBPF programs per-kernel, which is painful. Ring buffers, which replaced perf buffers and halved memory overhead in our tests, require 5.8. BPF iterators for walking kernel data structures require 5.9. Run kernel 5.15 LTS at minimum; 6.1 LTS is what we run in production and what all examples here were tested against.

Verify your kernel has BTF enabled before touching any tooling:

uname -r
# Should be 5.15 or higher

ls /sys/kernel/btf/vmlinux
# If this file exists, BTF is enabled

zgrep CONFIG_DEBUG_INFO_BTF /proc/config.gz
# Should return CONFIG_DEBUG_INFO_BTF=y

# Also confirm BPF JIT is on (required for acceptable performance)
cat /proc/sys/net/core/bpf_jit_enable
# 0 = disabled, 1 = enabled, 2 = enabled + tracing

The Tool Landscape: BCC, bpftrace, and libbpf

Three layers of tooling sit on top of the kernel eBPF subsystem and they serve different purposes. BCC (BPF Compiler Collection) is a Python/Lua frontend that ships with 70+ ready-made tools under /usr/share/bcc/tools/. It requires LLVM, Clang, and kernel headers at runtime, which makes it unsuitable for minimal production containers but fine for debugging hosts. bpftrace is a high-level tracing language closer to DTrace in style; one-liners for quick investigations. libbpf is the C library for writing production-grade, portable eBPF programs using CO-RE.

For interactive debugging sessions we reach for bpftrace. For persistent, always-on observability agents we write in C against libbpf. BCC sits in the middle and is most useful when someone has already written the BCC tool you need, which is often the case.

Install all three on Ubuntu 24.04 or Debian 12:

# BCC tools
apt install -y bpfcc-tools linux-headers-$(uname -r) python3-bpfcc

# bpftrace
apt install -y bpftrace

# libbpf development headers
apt install -y libbpf-dev

# Verify bpftrace version (we tested with 0.21.x)
bpftrace --version

# List available probes (expect 100k+ on a typical system)
bpftrace -l | wc -l

Five bpftrace One-Liners Worth Memorizing

These are the commands we actually type during incident response. Each one gives you data that would otherwise require a custom strace run or a profiler attached to a specific PID.

Trace all open() syscalls and print the filename, process name, and latency in microseconds. This is how we caught a misconfigured application opening the same config file 40,000 times per second:

# Syscall open latency per process
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @start[tid] = nsecs; }
  tracepoint:syscalls:sys_exit_openat /@start[tid]/ {
    @usecs[comm] = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
  }'

# Top disk I/O by process (read + write bytes per second)
bpftrace -e 'tracepoint:block:block_rq_complete {
    @bytes[args->comm] = sum(args->nr_sector * 512);
  } interval:s:1 { print(@bytes); clear(@bytes); }'

# TCP connections by destination port
bpftrace -e 'kprobe:tcp_connect {
    $sk = (struct sock *)arg0;
    @[ntop(2, $sk->__sk_common.skc_daddr),
      $sk->__sk_common.skc_dport] = count();
  }'

# CPU profile: sample stack traces at 99Hz for 30 seconds
bpftrace -e 'profile:hz:99 { @[kstack] = count(); }' --timeout 30

# OOM killer events with process name and pages freed
bpftrace -e 'tracepoint:vmscan:mm_vmscan_direct_reclaim_begin {
    printf("%s PID %d requesting reclaim\n", comm, pid);
  }'
// advertisement

Using BCC Tools for Network Observability

BCC ships with tcptracer, tcpretrans, tcpconnect, and tcpaccept. These are not replacements for Wireshark on a packet level, but they give you connection-level visibility with negligible overhead - we measured 0.3% CPU on a host handling 8,000 new TCP connections per second.

tcplife is particularly useful because it logs the full lifetime of every TCP connection including bytes transferred. Run it during a suspected connection leak:

# Full TCP connection lifecycle with duration and byte counts
/usr/share/bcc/tools/tcplife
# Output columns: PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS

# Trace TCP retransmits with stack trace
/usr/share/bcc/tools/tcpretrans -l

# Count new outbound connections per destination
/usr/share/bcc/tools/tcpconnect -c

# Trace DNS latency (UDP port 53)
/usr/share/bcc/tools/gethostlatency

# HTTP request latency by URL prefix (user-space uprobes)
/usr/share/bcc/tools/trace 'p::SSL_read(void *ssl, void *buf, int num) "%s", buf'

Writing a Minimal libbpf Program in C

When you need an observability agent that ships as a single binary with no runtime dependencies, libbpf with CO-RE is the answer. The pattern is: write a kernel-side BPF program in C, write a user-space loader in C using libbpf, compile with clang targeting bpf, generate a skeleton with bpftool, compile the loader against the skeleton.

Here is a minimal program that counts syscalls per PID and prints the top 10 every second. This is the skeleton of every production eBPF agent we have shipped:

// syscount.bpf.c - kernel-side program
#include 
#include 

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, u32);
    __type(value, u64);
} syscall_count SEC(".maps");

SEC("tracepoint/raw_syscalls/sys_enter")
int count_syscalls(struct trace_event_raw_sys_enter *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 *cnt = bpf_map_lookup_elem(&syscall_count, &pid);
    if (cnt) {
        __sync_fetch_and_add(cnt, 1);
    } else {
        u64 init = 1;
        bpf_map_update_elem(&syscall_count, &pid, &init, BPF_ANY);
    }
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

# Compile with:
# clang -g -O2 -target bpf -D__TARGET_ARCH_x86 \
#   -I/usr/include/$(uname -m)-linux-gnu \
#   -c syscount.bpf.c -o syscount.bpf.o

# Generate skeleton:
# bpftool gen skeleton syscount.bpf.o > syscount.skel.h

Integrating eBPF with Prometheus and Grafana

Raw bpftrace output is useful during an incident but not for long-term trend analysis. The standard production approach in 2026 is to expose eBPF metrics via a Prometheus exporter and visualize in Grafana. Two exporters are mature enough to use: ebpf_exporter from Cloudflare (github.com/cloudflare/ebpf_exporter) and Cilium's built-in Hubble for network observability.

ebpf_exporter takes a YAML config file that defines bpftrace programs and maps their output to Prometheus metrics. On our test server running Ubuntu 24.04 with 6.1 kernel, ebpf_exporter 2.3.0 added 1.1% CPU and 45MB RSS while tracking 12 concurrent eBPF programs.

Install and configure ebpf_exporter:

# Download ebpf_exporter 2.3.0
curl -LO https://github.com/cloudflare/ebpf_exporter/releases/download/v2.3.0/ebpf_exporter-2.3.0.tar.gz
tar -xzf ebpf_exporter-2.3.0.tar.gz
cd ebpf_exporter-2.3.0

# Copy example configs
cp -r examples /etc/ebpf_exporter

# Run with the bio (block I/O) example
./ebpf_exporter --config.dir=/etc/ebpf_exporter --config.names=bio

# Verify metrics are exposed
curl -s http://localhost:9435/metrics | grep ebpf

# Example systemd unit
cat > /etc/systemd/system/ebpf_exporter.service << 'EOF'
[Unit]
Description=eBPF Prometheus Exporter
After=network.target

[Service]
ExecStart=/usr/local/bin/ebpf_exporter \
  --config.dir=/etc/ebpf_exporter \
  --config.names=bio,tcp,runqlat
Restart=always
CapabilityBoundingSet=CAP_BPF CAP_NET_ADMIN CAP_PERFMON
AmbientCapabilities=CAP_BPF CAP_NET_ADMIN CAP_PERFMON

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload && systemctl enable --now ebpf_exporter
// advertisement

eBPF in Kubernetes: Cilium and Hubble

If you are running Kubernetes, Cilium is the most complete eBPF-based CNI and observability platform available in 2026. It replaces iptables with eBPF programs for packet processing, dropping latency by 30-40% in our benchmarks on a 3-node cluster running 500 pods. Hubble is Cilium's observability layer and gives you L3/L4/L7 visibility without any sidecar proxies.

Install Cilium with Hubble enabled on an existing cluster using the Cilium CLI:

# Install Cilium CLI
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -LO "https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-amd64.tar.gz"
tar -xzf cilium-linux-amd64.tar.gz && mv cilium /usr/local/bin/

# Install Cilium 1.16 with Hubble
cilium install --version 1.16.0 \
  --set hubble.relay.enabled=true \
  --set hubble.ui.enabled=true

# Verify status
cilium status --wait

# Enable Hubble port-forward and observe flows
cilium hubble port-forward &
hubble observe --namespace production --follow

# Filter by HTTP status codes
hubble observe --namespace production \
  --protocol http \
  --verdict DROPPED

# Show top services by request count
hubble observe --namespace production \
  --type l7 -o json | jq '.flow.l7.http.url' | sort | uniq -c | sort -rn | head -20

Continuous Profiling with Parca and eBPF

Continuous profiling captures CPU flame graphs from all processes at a low sample rate indefinitely, so you can go back in time and find what was consuming CPU during an incident. Parca Agent uses eBPF to do system-wide profiling at 19Hz with around 1% CPU overhead. It does not require language runtimes or instrumentation - it reads DWARF symbols directly.

Parca consists of a server (storage and UI) and an agent (eBPF profiler). Both ship as single binaries.

# Install Parca server (stores profiles, serves UI)
PARCA_VERSION=0.19.0
curl -LO https://github.com/parca-dev/parca/releases/download/v${PARCA_VERSION}/parca_${PARCA_VERSION}_Linux_x86_64.tar.gz
tar -xzf parca_${PARCA_VERSION}_Linux_x86_64.tar.gz
mv parca /usr/local/bin/

# Minimal config
cat > /etc/parca/parca.yaml << 'EOF'
object_storage:
  bucket:
    type: FILESYSTEM
    config:
      directory: /var/lib/parca
limit_active_series: 2048
EOF

# Install Parca Agent (runs on every node)
PARCA_AGENT_VERSION=0.31.0
curl -LO https://github.com/parca-dev/parca-agent/releases/download/v${PARCA_AGENT_VERSION}/parca-agent_${PARCA_AGENT_VERSION}_Linux_x86_64.tar.gz
tar -xzf parca-agent_${PARCA_AGENT_VERSION}_Linux_x86_64.tar.gz
mv parca-agent /usr/local/bin/

# Run agent pointing at Parca server
parca-agent \
  --node=prod-host-01 \
  --remote-store-address=parca.internal:7070 \
  --remote-store-insecure

# UI available at http://parca.internal:7070
# Navigate to CPU > select process > view flamegraph

Security Observability with Tetragon

Tetragon, from Isovalent (the company behind Cilium), uses eBPF for runtime security observability. It hooks into kernel functions to generate events for file access, privilege escalation, network connections, and process execution - with the option to enforce policy and kill processes directly from the eBPF program. This is different from seccomp or AppArmor because enforcement happens in the kernel before the action completes.

Install Tetragon standalone on a host (Kubernetes deployment also available via Helm):

# Install Tetragon 1.2.0 on a non-Kubernetes host
TETRAGON_VERSION=1.2.0
curl -LO https://github.com/cilium/tetragon/releases/download/v${TETRAGON_VERSION}/tetragon-v${TETRAGON_VERSION}-amd64.tar.gz
tar -xzf tetragon-v${TETRAGON_VERSION}-amd64.tar.gz
cd tetragon-v${TETRAGON_VERSION}-amd64
./install.sh

# Watch all exec events system-wide
tetra getevents -o compact --event-types PROCESS_EXEC

# Watch privilege escalation (setuid, capset)
tetra getevents -o compact --event-types PROCESS_KPROBE | grep -E 'setuid|capset'

# Apply a TracingPolicy to detect writes to /etc/passwd
cat > /etc/tetragon/passwd-write.yaml << 'EOF'
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: detect-passwd-write
spec:
  kprobes:
  - call: "security_file_permission"
    syscall: false
    args:
    - index: 0
      type: file
    - index: 1
      type: int
    selectors:
    - matchArgs:
      - index: 0
        operator: Equal
        values:
        - "/etc/passwd"
      - index: 1
        operator: Equal
        values:
        - "2" # MAY_WRITE
EOF
tetra tracingpolicy add /etc/tetragon/passwd-write.yaml
// advertisement

Debugging eBPF Programs and Common Errors

eBPF programs go through a kernel verifier before they run. The verifier rejects programs with unbounded loops, invalid memory accesses, or missing null checks. The error messages are terse. These are the errors we hit most often and how to fix them.

The most frequent issue for newcomers is the verifier rejecting a pointer dereference without a null check. Any map lookup returns a nullable pointer; the verifier requires you to check it before dereferencing. The second most common is exceeding the instruction limit, which was 4096 instructions before kernel 5.2 and is 1 million instructions since then.

Use bpftool to inspect loaded programs and maps:

# List all loaded eBPF programs
bpftool prog list

# Dump the bytecode of a specific program (by ID)
bpftool prog dump xlated id 42

# Show jitted machine code
bpftool prog dump jited id 42

# List all eBPF maps
bpftool map list

# Dump contents of a map (e.g., syscall count hash map)
bpftool map dump id 7

# Show all pinned objects in the BPF virtual filesystem
ls -la /sys/fs/bpf/

# Common verifier error: read from invalid pointer
# Fix: always null-check after bpf_map_lookup_elem
# WRONG:
#   u64 *val = bpf_map_lookup_elem(&map, &key);
#   return *val;  // verifier rejects this
# RIGHT:
#   u64 *val = bpf_map_lookup_elem(&map, &key);
#   if (!val) return 0;
#   return *val;

# Enable verbose verifier output when loading a program
bpftool prog load myprog.bpf.o /sys/fs/bpf/myprog 2>&1 | head -50

Automating eBPF Deployments in CI/CD

Shipping eBPF programs through a CI/CD pipeline requires a few specific steps beyond a normal binary: you need to run tests with root privileges or inside a VM, you need to ensure the test kernel version matches your production target, and you need to pin the libbpf version to avoid CO-RE incompatibilities.

For teams building eBPF-powered internal tools - for instance, a custom network policy enforcer or a performance profiling daemon - the operational scaffolding around deployment and alerting can be significant. Platforms like taskbotshub.ai can help automate the repetitive DevOps work around eBPF agent rollouts, including drift detection across node pools and automated rollback when an agent fails its health check.

A minimal CI pipeline for an eBPF project using GitHub Actions:

# .github/workflows/ebpf.yml
name: eBPF Build and Test
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y clang llvm libelf-dev libbpf-dev \
            linux-headers-$(uname -r) bpftool make

      - name: Build eBPF objects
        run: make -C src/ all

      - name: Verify eBPF objects pass verifier
        run: |
          for obj in src/*.bpf.o; do
            echo "Verifying $obj"
            sudo bpftool prog load "$obj" /sys/fs/bpf/test_prog \
              && sudo rm -f /sys/fs/bpf/test_prog \
              || exit 1
          done

      - name: Run integration tests (requires root)
        run: sudo -E go test ./tests/... -timeout 120s -v

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: ebpf-binaries
          path: dist/