eBPF programs don’t just magically run; they consume CPU cycles, and ignoring that consumption is the fastest way to destabilize your production systems.

Let’s see what a simple eBPF program looks like in action. Imagine we have a program that counts how many times a specific syscall, say openat, is called.

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u64));
} syscall_counts SEC(".maps");

SEC("kprobe/sys_openat")
int kprobe_sys_openat(struct pt_regs *ctx) {
    u32 key = 0;
    u64 *count;

    count = bpf_map_lookup_elem(&syscall_counts, &key);
    if (count) {
        (*count)++;
    }
    return 0;
}

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

This C code, when compiled into an eBPF object, can be loaded into the kernel. We can then attach it to the sys_openat kernel function. When sys_openat is called, our eBPF program executes. It looks up a counter in a BPF map, increments it, and returns.

The fundamental problem eBPF solves is allowing safe, in-kernel execution of user-defined code without modifying kernel source or loading modules. This is achieved through a verifier that ensures programs are safe (won’t crash the kernel, won’t loop infinitely) and a JIT compiler that translates the BPF bytecode into native machine code for performance.

The core components you directly control are:

  • Attachment Points: Where your eBPF program hooks into the kernel. This could be a kprobe (kernel function entry/exit), tracepoint (predefined kernel trace points), XDP (network packet processing at the driver level), or socket filter. Each has different performance characteristics and use cases.
  • BPF Maps: Data structures that eBPF programs can use to store state, share data with userspace, or communicate with other eBPF programs. Types include hash maps, arrays, LRU deques, etc. The efficiency of map operations is critical.
  • Program Logic: The actual instructions your eBPF program executes. This is where most overhead is introduced. Complex computations, excessive map lookups, or inefficient data handling will increase CPU usage.
  • Helper Functions: The limited set of kernel functions your eBPF program can call. These are designed to be efficient, but even calling them has a cost.

When you attach that kprobe/sys_openat program, every single time openat is invoked on your system, your eBPF code runs. If openat is called thousands or millions of times per second, even a few dozen instructions in your eBPF program can add up. The verifier and JIT ensure safety and performance, but they don’t eliminate the fundamental cost of execution.

The one thing most people don’t realize is how quickly the overhead of map operations can become the dominant factor. A simple bpf_map_lookup_elem followed by bpf_map_update_elem might seem trivial, but each involves context switches, memory accesses, and potential contention. If your program is doing this in a high-frequency path, the cumulative cost can be substantial, even if the individual instructions are fast.

Understanding that eBPF programs are just code running in the kernel, subject to the same CPU resource constraints as any other kernel code, is the next hurdle.

Want structured learning?

Take the full Ebpf course →