What Is eBPF-Based Instrumentation in OpenTelemetry?
A plain-language guide to collecting traces, metrics and profiles by watching programs from inside the operating system, with no code changes, no restarts and no language-specific agents.
Picture a company with three hundred services. Some are written in Go, some in Java, some in Python, and a few in C++ that nobody has touched for years. A handful are programs bought from vendors, delivered as a single file with no source code at all. Leadership wants to see how every request moves through every service, and wants it by next quarter. The traditional answer is to add monitoring code to each program, one language at a time. For this company, that answer means months of work, and for the vendor programs it means no answer at all.
eBPF offers a different route. It is a technology built into the Linux operating system that lets small, safe programs watch what other programs are doing, from the outside, as it happens. OpenTelemetry projects use that ability to produce standard telemetry without asking the observed programs to change. The result is a way to get useful traces, metrics and performance profiles for software you cannot or do not want to modify.
This guide explains eBPF from scratch, shows how OpenTelemetry uses it, and is honest about its limits. You do not need to know anything about operating system internals. Every new term is explained when it first appears, and each big idea comes with an everyday comparison.
AThe Problem: Watching Code You Cannot or Will Not Change
Good observability starts with data, and data starts with instrumentation. The hardest part is often getting that instrumentation into place.
What Instrumentation Means
Instrumentation is the act of making a program report on itself. A program that is instrumented creates traces, which follow a single request across many services; metrics, which are numbers measured over time such as requests per second; and logs, which are timestamped records of events. Together these are called telemetry. OpenTelemetry is the open-source project that defines standard ways to create, name and send it.
The usual way to instrument is to add a library to your code and call it at the interesting moments. That works well when you own the code, know the language, and have time. In practice, one or more of those conditions often fails.
Three Common Obstacles
No source code
Vendor products, old binaries and third-party tools arrive without code you can change. You can run them, but you cannot edit them.
Many languages
Each language needs its own library, its own setup and its own expertise. A fleet of six languages means six projects.
No one has capacity
Teams with product deadlines rarely volunteer to add monitoring code to stable services that are not currently on fire.
Fear of change
Touching a critical service just to add telemetry can feel riskier than living without it, especially for old systems.
Imagine an office building with fifty tenants. One way to track who comes and goes is to ask every tenant to keep a visitor log at their own door. That gives rich detail, but it needs fifty tenants to cooperate. Another way is to let the building’s front desk record everyone who walks through the lobby and take the elevator. The desk never sees what happens inside a room, but it needs no cooperation from anybody. eBPF-based instrumentation works like the front desk.
What Teams Want From a Solution
- Fast coverage. See something useful for every service within days, not quarters.
- No code changes. Do not edit, rebuild or redeploy the programs being watched.
- Language independence. One approach that works the same way for any language.
- Standard output. Data in OpenTelemetry’s standard format, so that existing tools and dashboards keep working.
- Low overhead. Little extra load on already busy production machines.
eBPF fits the front-desk role well, and it is most valuable when combined with in-code instrumentation for the details only the code can know. Later chapters explain exactly where each approach shines.
BThree Ways to Collect Telemetry
Before looking at eBPF, it helps to see where it sits among the other options.
Option One: Manual Instrumentation With an SDK
An SDK, short for software development kit, is the OpenTelemetry library that lives inside your program. With manual instrumentation, developers call it directly: start a span here, record a metric there, attach the customer tier as an attribute. It gives the richest, most meaningful data, because the people who wrote the code decide exactly what matters. It also needs the most effort and must be repeated for every language.
Option Two: In-Process Auto-Instrumentation
Many languages let an agent attach to a running program and add instrumentation automatically to popular libraries. The Java agent, which hooks into the Java runtime, is the best-known example, and similar tools exist for Python, Node.js and .NET. Developers change no code, but they must add the agent to every service, and each language has its own agent with its own quirks. The agent lives inside the program’s process and shares its memory and CPU.
Option Three: eBPF-Based Instrumentation
With eBPF, a separate program runs next to the application, often one per machine, and installs tiny probes in the operating system’s kernel. These probes watch system events, such as a network read, and the moments when certain functions in the application are called. The separate program turns those observations into standard OpenTelemetry spans and metrics. The application is unaware and unchanged.
| Approach | Where it runs | Code changes | Depth of detail | Language dependence |
|---|---|---|---|---|
| Manual SDK | Inside the program | Yes, throughout | Deepest; business meaning | One library per language |
| In-process agent | Inside the program | None, but each service is configured | Deep for supported libraries | One agent per language |
| eBPF-based | Outside the program, in the kernel and a helper | None | Request-level; protocol and function view | Mostly independent |
Think of three ways to know what happens in a restaurant. A manual SDK is the chef narrating each step of every dish. An in-process agent is a camera mounted in the kitchen that understands recipes in one cuisine. eBPF is a sensor at the entrance and at the pass-through window, counting orders, timing how long dishes take and noting which table receives them, without knowing a thing about cooking.
eBPF is not “better” than SDKs, and it does not replace them. It trades depth for reach. It excels at breadth, but it cannot learn that a request belongs to a premium customer unless that fact appears somewhere it can observe.
CWhat Is eBPF?
The name sounds technical, but the idea is simple: run small, checked programs inside the operating system without modifying it.
A Short History
The story starts in 1992, when researchers designed the BSD Packet Filter, known as BPF. Its job was narrow: let a tool like tcpdump tell the kernel which network packets it wanted, so that the kernel could discard the rest early and cheaply. BPF was a tiny virtual machine inside the kernel, and its design proved sound for decades.
Around 2014, engineers extended it dramatically for Linux. The new version, extended BPF or eBPF, supports far more than packet filtering. Programs can be attached to many kinds of events, can remember information between events, and can pass data to ordinary programs. Despite the name, eBPF today is a general-purpose way to safely extend the kernel, and the letters are treated as a name, not an abbreviation.
What the Kernel Is, and Why Programs Want to Watch It
The kernel is the core of the operating system. It manages memory, schedules which program runs, talks to disks and network cards, and mediates every request an application makes of the machine. Every file opened, every network connection made and every byte sent passes through it. That makes the kernel a perfect observation post: if you can watch it, you can see what every program on the machine is doing.
Historically, extending the kernel meant either changing its source code, a slow process involving the whole operating system community, or loading a kernel module, a plug-in with full power that can crash the entire machine if it has a bug. eBPF offers a third path.
A phone’s app store is a good picture. Apps can do useful things with the phone’s sensors and settings, but only after a review checks that they will not crash the phone or steal data. eBPF programs are like reviewed apps for the kernel: they get access to events in the system, but only after an automatic checker proves they are safe.
Safety Through the Verifier
The key to eBPF is the verifier, a checker built into the kernel. Before any eBPF program is allowed to run, the verifier analyzes every possible path through it. It rejects programs that could loop forever, read memory they should not, or crash the system. A program that passes is compiled into native machine code by a just-in-time compiler, so it runs at close to full speed.
The price of this safety is restriction. eBPF programs must be small and must finish quickly. They cannot wait for a disk or sleep. They cannot call arbitrary kernel functions, only a limited set of approved helpers. That limited power is exactly what makes them safe to run in production.
Where eBPF Is Used Today
Observability is only one use. eBPF powers networking, where projects such as Cilium route and filter traffic inside the kernel, and large-scale load balancers and attack filters process packets at very high speed. It powers security tools that watch for suspicious system calls. Performance engineers have used it for years to answer questions like “why is this machine slow?” without installing heavy tools. OpenTelemetry’s use of it is a natural extension of that story.
DHow an eBPF Program Runs, Step by Step
Following one probe from creation to data delivery explains most of the vocabulary you will meet.
flowchart TD A["Developer writes a small program
in restricted C or Rust"] --> B["Compiler produces eBPF bytecode"] B --> C["User-space loader asks the kernel
to load the program"] C --> D{"Verifier checks safety"} D -->|"Rejected"| X["Load fails with an error"] D -->|"Accepted"| E["Compiled to native code"] E --> F["Attached to a hook
for example a function call or a network read"] F --> G["Event happens and the program runs"] G --> H["Results written to a map or ring buffer"] H --> I["User-space agent reads the data"]
The Steps in Plain Words
Write and compile
The program is written in a restricted style of a systems language and compiled into a compact instruction set that the kernel understands.
Load
An ordinary program running in user space, the normal area where applications live, asks the kernel to accept the new code. This requires special permissions.
Verify
The kernel’s verifier proves the code cannot misbehave. Failure here is common during development and gives detailed error messages.
Attach
The program is connected to a hook, a specific kind of event, such as “a function in this application was called”.
Run on events
Each time the event happens, the kernel runs the program for a few microseconds. It reads a few values, perhaps the arguments of the call and the current time.
Share the result
The program stores what it saw in a shared structure, and a user-space agent collects it.
Maps and Ring Buffers
eBPF programs cannot hold information in ordinary variables between runs, because each run is a fresh, short event. Instead they use maps, shared tables managed by the kernel. A program can record “request 42 started at this time” in a map when a call begins, and look it up when the call ends to compute the duration. Maps are also how user-space and kernel programs exchange settings, such as which processes to watch.
To send a stream of observations out, programs write to a ring buffer, a fixed-size circular area that the agent reads. If the agent falls behind and the buffer fills, new events are dropped. That is a design trade-off to remember: the kernel side never waits, so a slow reader loses data instead of slowing the system.
Compile Once, Run Everywhere
Kernels differ in the exact layout of their internal data. A program written for one kernel version may look in the wrong place on another. The solution, called CO-RE for compile once, run everywhere, relies on a description of the kernel’s own data layout, called BTF. At load time the loader adjusts the program to match the machine it runs on. This is why modern eBPF agents can ship one build for many kernels, and why they usually require a kernel that exposes BTF.
When a team says “our agent does not start on that host”, the first two questions are whether the kernel is new enough and whether it exposes BTF. Those two answers explain most compatibility issues before you read a single log.
EHooks: Where eBPF Listens
An eBPF program does nothing until it is attached to an event. The choice of hook decides what the instrumentation can see.
The Main Hook Types
| Hook | What it watches | Typical observability use | Stability |
|---|---|---|---|
| Kernel probe | Entry or exit of a function inside the kernel | Network reads and writes, connection setup | Tied to kernel function names, which can change |
| Tracepoint | A deliberately placed, documented event in the kernel | Process start, scheduling, system calls | Most stable, maintained as an interface |
| User-space probe | Entry or exit of a function inside an application or library | Request handlers, TLS libraries, language runtime functions | Tied to the application’s symbols and version |
| Static user probe | Markers placed by a library’s authors | Database or runtime events where available | Stable when provided |
| Performance event | A timer or hardware counter firing at a regular rate | Sampling profilers | Stable |
| Network hooks | Packets or sockets as they pass through | Flow metrics, header handling | Stable, with kernel-version differences |
Think of sensors in a factory. Tripwires on doors fire whenever something passes, like kernel and user-space probes. A security guard making rounds every ten minutes is like a sampling profiler, taking a snapshot at a steady rhythm. A toll booth on the road checks every vehicle’s size and speed, like network hooks. Each sensor suits a different question.
User-Space Probes and Their Fragility
User-space probes are the tool that lets eBPF understand application behaviour. A probe can be attached to a specific function in a program, so that each time the function runs, the eBPF code can read its arguments. To place such a probe, the agent must know where the function lives in the program’s file, usually by reading its symbol table, the list of function names and addresses.
This creates dependencies. If a binary is stripped of symbols to save space, probes cannot find their targets. If a new version of a library renames or reorganizes a function, probes written for the old layout may fail or read the wrong data. The instrumentation must be updated whenever supported libraries change enough to move things around. This is the main reason that probes into application code need constant maintenance, while probes on stable kernel events change rarely.
Kernel Probes and Stable Surfaces
Probes at the kernel level, such as those watching network reads and writes, are more portable across applications because every program uses the same system interfaces. A web server in any language eventually calls the kernel to read bytes from a connection. That common surface is what lets a single agent observe programs written in many languages. The agent then examines the bytes to recognize protocols such as HTTP, which is the topic of later chapters.
It is tempting to believe that “eBPF sees everything”. It sees what passes through the hooks it attached to. If the information never touches a hook, or touches it in a form the agent cannot interpret, it is invisible.
FThe Moving Parts of an eBPF-Based Agent
An eBPF instrumentation tool is not just a few kernel probes. It is a small system with a kernel half and a user-space half, each doing the jobs it is best at.
flowchart LR
subgraph Node["One machine"]
APP["Applications
any language"] --> K["Kernel with eBPF probes"]
K --> RB["Ring buffers and maps"]
RB --> AG["User-space agent
builds spans and metrics"]
end
AG -->|"OTLP"| COL["Collector"]
COL --> BE["Backend"]
The Kernel Half
The kernel half is a set of small eBPF programs attached to hooks. Their job is deliberately narrow: notice that something happened, capture a few facts, and hand them over quickly. They record things like the time, the process that triggered the event, the connection involved and the first bytes of a message. They do not try to understand what they see, because they are limited in size and cannot do heavy work.
The User-Space Half
The user-space half is an ordinary program, often written in Go or Rust, running as a normal process. It loads and attaches the probes, reads the ring buffers, and does the heavy thinking that the kernel side may not do. It parses protocol bytes into requests and responses, pairs them up to compute durations, builds OpenTelemetry spans and metrics with standard names, enriches them with information such as the Kubernetes pod name, and exports them using OTLP, the OpenTelemetry Protocol, usually to a Collector.
| Part | Job | Runs where |
|---|---|---|
| Probes | Notice events and capture raw facts | Inside the kernel |
| Maps and ring buffers | Hold state and carry events outward | Kernel memory shared with user space |
| Loader and discovery | Attach probes to the right processes as they start and stop | Agent |
| Parsers | Turn raw bytes into requests, responses and operations | Agent |
| Span and metric builder | Create standard telemetry from the parsed events | Agent |
| Metadata enricher | Add service, pod, namespace and node details | Agent |
| Exporter | Send data through OTLP | Agent |
Consider a news agency with field reporters and an editorial desk. Reporters at the scene send short, quick notes: who, where, when. They have no time to write polished stories. The desk receives the notes, checks facts, adds background and publishes a finished article. The kernel probes are the reporters, and the user-space agent is the desk.
Why Split the Work This Way
Doing as little as possible inside the kernel keeps programs inside the verifier’s limits and keeps the risk low. If the user-space half crashes, the application keeps running, because it never depended on the agent. If a parser has a bug, it affects the agent’s output, not the stability of the machine. And updating parsing logic is as easy as updating an ordinary program, with no kernel changes required.
When evaluating any eBPF-based tool, ask which tasks happen in the kernel and which in user space. A design that keeps the kernel side small is easier to trust, easier to port between kernels and easier to reason about under load.
GOpenTelemetry’s eBPF Projects
Several projects bring eBPF into the OpenTelemetry family. They differ in what they watch and in how mature they are.
Go Automatic Instrumentation
The OpenTelemetry Go automatic instrumentation project targets programs written in Go. Go programs are compiled into a single native file with no runtime agent mechanism like Java’s, so a classic in-process agent is not possible. The project attaches user-space probes to well-known functions in popular Go libraries, such as the standard web server and client, database access, and common messaging and RPC libraries. When those functions run, the probes read their arguments and produce spans. It usually targets a single process identified by the path of its executable, and it can be deployed in Kubernetes through the OpenTelemetry Operator or as a companion container.
OpenTelemetry eBPF Instrumentation
The second effort is a language-independent tool. It began as Beyla, a project from Grafana Labs, and as far as is publicly known it was contributed to the OpenTelemetry project and continues there as OpenTelemetry eBPF Instrumentation, often abbreviated OBI. It watches kernel-level activity to recognize web, RPC and database traffic from programs in many languages and produces spans and request metrics. It can discover which processes to watch using listening ports, executable names or Kubernetes labels, and it understands Kubernetes metadata. Because the project is evolving quickly, check its current documentation for supported protocols and kernel requirements.
The eBPF Profiler
The third project is a continuous profiler, contributed to OpenTelemetry from a commercial profiling agent. Instead of watching requests, it samples what code is running on each CPU many times per second across the whole machine and builds a picture of where time is spent. It is notable for walking the call stacks of native code and several language runtimes, directly from eBPF, without needing special compile options in the programs. Profiles are a newer OpenTelemetry signal that is still being defined, so expect changes.
Network Observability and the Wider Ecosystem
Other work focuses on network observability: watching connections, bytes and retransmissions between workloads to build maps of who talks to whom. Beyond OpenTelemetry’s own repositories, an ecosystem of tools uses eBPF and can export OTLP. Examples include Kubernetes-focused observability platforms and distributions that mix eBPF for some languages with in-process agents for others, plus offerings from commercial vendors. Each has its own strengths, and their behaviour does not always match the OpenTelemetry projects exactly.
| Project | What it observes | Main signals | Things to check |
|---|---|---|---|
| Go automatic instrumentation | Go libraries inside chosen Go processes | Traces | Supported Go versions and libraries; one process per instance |
| eBPF instrumentation (OBI) | Protocol traffic from many languages | Traces, request metrics | Protocol support; kernel features; context propagation behaviour |
| eBPF profiler | CPU activity of the whole machine | Profiles | Runtime support; the profiles signal’s stability |
| Network-focused tools | Connections and flows | Metrics | Project activity and cardinality of flow data |
Names, ownership and maturity of these projects have changed over time and are still changing. Before choosing one, read its current repository notes for stability level, supported kernels, supported protocols and known limitations, instead of relying on any summary, including this one.
HHow Telemetry Appears Without Touching Code
The magic has a mechanism. Understanding it explains both the strengths and the limits of the approach.
Recognizing Protocols From Bytes
When a program receives an HTTP request, the bytes arrive through the kernel. A probe on the kernel function that delivers those bytes can see the first part of each message. The user-space agent examines it. HTTP requests begin with a recognizable pattern: a method, a path and a version. The agent can tell that a request arrived, which method and path it used, and, when the program writes its reply, which status code it sent. The time between the two is the request’s duration. This works for any language, because any web server must read and write through the kernel.
The same idea extends to other protocols: HTTP/2 and gRPC, several database protocols, caches and message brokers. Each needs a parser that understands that protocol’s message layout, so supported protocols are a key feature to check in any tool.
A court reporter in a courtroom records who spoke, when, and for how long, and can even note the words said. The reporter does not know what the lawyers are thinking, why the case matters or how the judge will rule. The reporter sees the public surface of the proceedings, which is enough to produce a precise timeline.
Seeing Inside Encrypted Traffic
Most production traffic is encrypted. At the kernel level, bytes on the connection are scrambled and cannot be read. The workaround is to attach user-space probes to the functions in the encryption library, such as the widely used OpenSSL or the Go standard library’s encryption, at the point where plain data is handed over for encryption or has just been decrypted. This yields readable requests without breaking encryption on the wire. It depends on the application using a library the agent knows how to hook, which is one more compatibility question to ask.
Hooking Language-Level Functions
For some languages, especially Go, user-space probes can be attached directly to functions in common libraries. The probe reads the function’s arguments and sees structured information, such as the parsed request object, rather than raw bytes. That gives a more exact view, at the cost of depending on library versions and internal data layouts.
From Events to Standard Telemetry
Pair start and end
A request seen at time A and a response seen at time B become one operation with a duration.
Build a span
The operation becomes a span, a server span for a request arriving or a client span for a request leaving, with the method, status and address as standard attributes.
Derive metrics
Counting operations by status gives request rate and error rate. Recording durations in a histogram gives latency distributions. Together these are known as the RED metrics: rate, errors and duration.
Name things properly
Attributes follow OpenTelemetry’s semantic conventions, the standard dictionary of names, so dashboards built for SDK data work with eBPF data too.
Check which edition of the semantic conventions your eBPF agent follows. If your SDK-instrumented services use a different edition, dashboards that filter on attribute names will show only part of the fleet until you reconcile them.
IFollowing One Request Through an eBPF Agent
A single request, traced step by step, shows exactly where the kernel probes and the agent do their work.
sequenceDiagram
participant C as Caller
participant K as Kernel probes
participant A as Application
participant G as eBPF agent
participant O as Collector
C->>A: HTTP request arrives
K->>K: Note read event and start time
A->>A: Handles the request
A->>C: Response is written
K->>G: Event with timing and first bytes
G->>G: Parse request and build span
G->>O: Send span and metrics over OTLP
The Story in Eight Steps
A request arrives
A caller sends an HTTP request to the checkout service. The bytes pass through the network stack into the kernel.
A probe records the start
A probe on the read path stores the current time in a map, keyed by the connection and the process.
The application works
The checkout service handles the request. The probes do nothing unless the service reads or writes something relevant.
A downstream call leaves
The service calls the inventory service. A probe on the write path notes a client operation starting.
The downstream call returns
When the inventory response is read, the probe computes the client duration and emits an event.
The response is written
The service writes its answer. A probe notes the end time and emits an event with the timing and the first bytes.
The agent builds spans
In user space, the agent parses the bytes, matches start with end and forms a server span for the request and a client span for the downstream call.
Export
Spans and metric updates are batched and sent to the Collector. Total extra work in the request path was a few microseconds per probe hit.
What Is Missing From the Story
Notice what the agent knows and what it does not. It knows the method, path, status, timing and the endpoints of each connection. It does not know the customer’s identity, the order’s value, which branch of the code ran, or why the downstream call happened. It also has to decide how the server span and the client span relate, and how this trace connects to the caller’s trace on another machine. That last question is difficult enough to need its own chapter.
JThe Hard Problem: Context Propagation
A trace is only a trace if its pieces connect. Making them connect from outside the application is the hardest thing eBPF instrumentation does.
What Context Propagation Is
When service A calls service B, they need to share an identifier saying “this call belongs to trace number 4711, and I am span number 9”. That information travels in a standard HTTP header called traceparent, defined by the W3C Trace Context standard. Service B reads the header and continues the same trace. This hand-off is called context propagation. Without it, each service creates its own separate, unconnected trace.
Imagine a relay race where each runner carries a numbered baton. The baton tells timekeepers which race the runner belongs to. If a runner drops the baton and picks up an unnumbered one, the timekeepers see many short sprints and no race. The header is the numbered baton.
Reading Is Easy, Writing Is Hard
Reading the incoming header is straightforward. The bytes are already present in the request, so the agent can read the trace identifier and attach its spans to the caller’s trace. Writing the header into an outgoing request is much harder. The bytes of the outgoing request belong to the application. To add a header, something must change those bytes before they are sent, and the safety rules that make eBPF trustworthy deliberately restrict how much a program may change.
How Tools Approach It
| Approach | How it works | Limits |
|---|---|---|
| Honor headers the application already sends | If the application or an SDK passes the header along, the agent reads and uses it | Requires some cooperation from the application |
| Runtime-specific injection | For some runtimes, probes cooperate with the application’s own data structures to add the header | Tied to specific languages and library versions |
| Network-level injection | Network hooks add the header to supported protocols in outgoing traffic | Needs certain kernel features; works only for some protocols and conditions |
| Best-effort linking | Match calls by connection details and timing, without headers | Approximate; can mislink under heavy concurrency |
The details differ between tools and change as projects mature, so check what your chosen tool supports on your kernel version and protocols.
What Happens When Propagation Fails
When the chain breaks, each service still gets accurate spans and metrics, but traces no longer cross service boundaries. You see many short traces instead of one long one. Service maps built from connection data still work, because they depend on who connected to whom, not on trace identifiers. Latency by service is still correct. What you lose is the ability to follow one specific request from front door to database.
Many teams get complete traces cheaply by making sure every service simply forwards the trace header it receives, which often needs only a small shared library or proxy setting, while eBPF produces the spans. The application passes the baton; the agent keeps the times.
Do not judge eBPF tracing by a quick demo of two services where everything is connected. Test your real call patterns, including asynchronous queues, proxies and load balancers, before promising end-to-end traces to stakeholders.
KLanguage Coverage: What Works for Which Language
The promise of “any language” is real at one level and limited at another. The difference depends on how each language runs.
Two Levels of Visibility
At the protocol level, the agent sees traffic crossing the kernel: requests, responses, timing and connection details. This works for almost any language, because every language eventually calls the same kernel functions. At the function level, the agent sees inside the application, such as the arguments of a specific library call. This depends on how the language is compiled and executed, and works only where probes can find stable function addresses.
| Language family | Protocol-level view | Function-level view | Notes |
|---|---|---|---|
| Go | Yes | Yes, for supported libraries | Compiled to native code; probes read arguments; sensitive to Go and library versions |
| C, C++, Rust | Yes | Possible when symbols are present | Stripped binaries hide function names; inlining removes probe points |
| Java, Kotlin, Scala | Yes | Limited | The runtime generates code on the fly, so stable function addresses are scarce; an in-process agent is far richer |
| Python, Ruby, PHP, Node.js | Yes | Limited | Interpreters run application code as data; in-process agents give deeper views |
| .NET languages | Yes | Limited | Similar to Java, with runtime-generated code |
A security guard at a building entrance does not need to speak every tenant’s language to record who enters and when. That is protocol-level visibility. Reading a note a tenant is writing at their desk requires understanding their language and being close enough to see it. That is function-level visibility, and it works only for languages the guard has studied.
The Concurrency Puzzle
To compute a request’s duration, the agent must match its start to its end. In old-style servers, one thread handles one request at a time, so matching by thread is easy. Modern programs handle many requests on the same thread, or move one request between threads: event loops in Node.js, goroutines in Go, async code in many languages. Matching must therefore rely on the connection a request arrived on, not on the thread. This works well for simple request-response protocols, and less well when many requests share one connection, as in HTTP/2 and gRPC, where the agent must also track stream identifiers.
When Depth Matters, Add an In-Process Agent
For Java and similar runtimes, a common pattern is to let the eBPF agent provide baseline coverage for everything, and attach the language’s own in-process agent only to the services where deeper detail repays the extra setup. The protocol-level data from eBPF still gives you a consistent view across the fleet, even for services that have no in-process agent.
Inventory your fleet by language and by criticality before choosing a plan. A rough table of “language, number of services, need for deep detail” quickly shows where eBPF alone is enough and where you need more.
LDeploying on Kubernetes
Kubernetes is where eBPF instrumentation is most often used, because one agent per machine can cover every pod on it.
The Node-Level Agent
Kubernetes organizes work into pods, each holding one or more containers, running on machines called nodes. A DaemonSet is a Kubernetes object that runs exactly one copy of a program on every node. Deploying the eBPF agent as a DaemonSet gives complete node coverage: when a new pod lands on a node, the local agent notices and begins watching it, with no change to the pod.
flowchart TD
subgraph N1["Node 1"]
P1["Pod A"] -.->|"observed by"| AG1["Agent pod from the DaemonSet"]
P2["Pod B"] -.->|"observed by"| AG1
end
subgraph N2["Node 2"]
P3["Pod C"] -.->|"observed by"| AG2["Agent pod from the DaemonSet"]
end
AG1 -->|"OTLP"| GW["Collector gateway"]
AG2 -->|"OTLP"| GW
GW --> BE["Backend"]
What the Agent Needs
- Visibility into other containers’ processes. By default a container sees only its own processes. The agent usually runs with access to the host’s process list so it can see and probe programs in neighbouring pods.
- Elevated permissions. Loading eBPF programs needs special capabilities, covered in the next chapter. Many deployments run the agent as a privileged container, and security-conscious ones list only the exact capabilities required.
- Access to kernel interfaces. The agent mounts a few host paths, such as the filesystem that holds eBPF objects and the file describing kernel data layouts.
- Read access to Kubernetes metadata. To label telemetry with pod and workload names, the agent asks the Kubernetes API, which needs a role granting read-only access.
A DaemonSet agent is like one night security officer assigned to each floor of a hotel. Whenever a new guest checks into a room on that floor, the officer for that floor automatically includes the room in their rounds. Nobody has to tell the guest or the room.
Choosing What to Watch
Watching every process on every node is rarely what you want. Agents let you narrow scope by listening port, executable name, namespace or labels. Starting narrow, for example one namespace, lets you check data quality and measure overhead before expanding. Excluding noisy infrastructure processes also keeps costs and cardinality under control.
Sidecar and Operator Options
Some projects, especially the Go instrumentation, can run beside a single application instead of per node. The OpenTelemetry Operator for Kubernetes can inject such helpers when a pod carries the right annotations, and shared process visibility inside the pod lets the helper see the application’s process. The per-pod approach gives tighter control and clearer ownership, at the price of more copies of the agent and more resource requests.
Resource requests and limits for the agent matter. If the agent is starved of CPU, it cannot drain its ring buffers fast enough, and events are dropped while the application looks perfectly healthy. Give agents a reasonable reservation and monitor their drop counters.
MKernel Requirements, Privileges and Compatibility
eBPF is powerful because it works at a low level, and that brings requirements that SDKs never have.
The Requirements Checklist
| Requirement | Why it exists | How to check |
|---|---|---|
| Linux host | The mature eBPF ecosystem lives on Linux; other systems have limited or different implementations | Confirm node operating systems in every cluster |
| Recent enough kernel | Features such as ring buffers and certain hooks arrived in specific kernel versions | Check the kernel release on each node; compare with the tool’s documentation |
| Kernel type information (BTF) | Lets one program build adapt to different kernels | Look for the kernel’s BTF file; ask the vendor if it is missing |
| Specific capabilities | Loading programs and attaching probes are privileged operations | Read the tool’s list and grant exactly those |
| Permission from security policy | Container runtime, mandatory access control and kernel lockdown can block eBPF | Test in a staging cluster with production policies |
| Symbols or runtime support | User-space probes need function names or a known runtime layout | Test sample binaries from each team |
Capabilities and Privilege
Linux divides administrator power into small pieces called capabilities. Newer kernels have capabilities specifically for loading eBPF programs and for performance monitoring, so agents no longer need full administrator power. Typical agents still ask for a short list that includes permission to load programs, to observe other processes and to read certain system files. The tool’s documentation lists the exact set. Granting only that list, instead of a blanket privileged mode, is a straightforward security improvement.
The Compatibility Matrix
Large organizations run a mix of kernels: new cloud images, older long-term-support enterprise distributions with features back-ported, and vendor-specific kernels. Build a simple matrix of operating system image, kernel version, BTF availability and security policy for each cluster. Test the agent on one machine from each row before a broad rollout. The results often show a few special cases, such as an older node pool that needs an upgrade or an exception.
Installing a new smart-home device requires the right plug, voltage and Wi-Fi standard. A device that works perfectly in one house may not work in an older one. Checking each house before installing saves frustrating surprises. The compatibility matrix is that house-by-house check for your clusters.
Where eBPF Agents Cannot Run
Some environments simply do not allow it. Serverless container platforms that give you no access to the underlying kernel cannot host a node-level agent. Some managed Kubernetes offerings restrict privileged workloads or hide the nodes. Local development on laptops with other operating systems usually runs a Linux virtual machine, which may or may not expose the features required. In these places, in-process instrumentation remains the practical choice.
Involve your security team early and give them a one-page summary: what the agent can see, which capabilities it needs, where data goes and how it can be disabled. Agents that are explained are approved far faster than agents that are discovered.
NIdentity and Metadata: Giving Telemetry a Name
An observation of “a request took 80 milliseconds” is useless until you know which service it belongs to. eBPF sees processes and sockets; people think in services and teams.
The Identity Gap
What eBPF sees at the kernel level is a process identifier, a binary name and a network address. What dashboards need is a service name, a namespace, a version, a deployment environment and an owner. Bridging this gap is the job of the agent’s metadata enrichment, and doing it well decides whether the data is usable.
Where the Names Come From
| Attribute | Typical source | Risk |
|---|---|---|
| Service name | An environment variable visible on the process, a Kubernetes label, the workload name, or the executable name as a last resort | Executable names such as “java” or “python” tell you nothing about the service |
| Namespace, pod and node | The Kubernetes API | Stale data if the API cache lags behind pod changes |
| Workload and owner | Labels or annotations on the deployment | Missing labels produce “unknown” ownership |
| Service version | Image tag or a label | “latest” tags hide real versions |
| Environment | Cluster or namespace conventions | Inconsistent naming between clusters |
Aligning With SDK-Produced Data
If some services use SDKs and others use eBPF, the same service should carry the same name in both worlds. Otherwise a dashboard for “checkout” shows half the story, and totals are wrong. Establish one naming rule, such as “the service name is the Kubernetes workload name unless an explicit label overrides it”, and apply it in the agent, in SDK configuration and in the Collector. Write the rule down and check it with a periodic report of names that appear in only one source.
A hospital might record a patient by hospital number in one system and by name in another. When both systems are combined, the patient appears twice unless someone ensures that one identifier links them. A consistent service name is the shared patient number across your telemetry sources.
Kubernetes Cost and Permissions
Enrichment from the Kubernetes API requires the agent to watch pods and other objects. On large clusters, hundreds of agents each watching everything can create real load on the API. Efficient agents cache data, watch only the objects on their own node, or delegate enrichment to a central Collector. Whichever design you choose, grant read-only roles, and test behaviour during large rollouts, when many pods change at once.
A service named after its program, such as “node” or “java”, is a signal that identity enrichment is not working. Count these generic names in your first week and fix the source of the naming before building any dashboards on top.
OConnecting the Agent to the Collector Pipeline
The agent is the first stop. The OpenTelemetry Collector is where shared policy is applied before data reaches the backend.
Where to Send the Data
The agent exports using OTLP to a Collector, either one running on the same node or a shared gateway pool. A Collector is a program with receivers that accept data, processors that change it, and exporters that send it onward. Sending through a Collector, rather than straight to a backend, lets you apply consistent rules to data from both eBPF agents and SDKs: filtering, renaming, sampling, redaction and routing.
A Sensible Processor Order
Memory limiter
First in line, so the Collector can refuse data before its memory fills up during a spike.
Enrichment
Add or correct resource details such as Kubernetes metadata, when the agent has not already done so.
Filtering and normalization
Drop health-check spans, replace raw URL paths with route templates, and align attribute names with your organization’s conventions.
Sampling
Reduce volume by keeping a share of traces, or the interesting ones, before they are stored.
Batching
Group items into efficient batches just before export, so you never batch what earlier steps would have discarded.
A factory’s loading dock has stages: a gate that limits how many trucks enter, a check that labels each box, a station that removes defective goods, and finally a crew that packs boxes into full trucks. Each stage is cheap on its own and effective in order. A Collector pipeline works the same way.
The Cardinality Problem Is Bigger With Protocol Parsing
An agent that reads request paths from raw bytes may see paths like /orders/8841, /orders/8842 and so on, with a different identifier each time. If these become metric attributes, each identifier creates a new time series, and the backend’s cost balloons. Good agents try to infer route templates, such as /orders/{id}, and good Collector pipelines add a normalization step as a safety net. Watch the number of distinct values of the route attribute during the first days of a rollout.
Mind the Edition of the Names
The agent emits attribute and metric names from some edition of OpenTelemetry’s semantic conventions, the shared dictionary of names. Your SDK-based services may emit a different edition. Check the schema URL, the label that identifies the edition, and normalize in the Collector where editions differ. A short comparison of the agent’s output against your registry or rule sheet, done before broad rollout, prevents surprises in dashboards and alerts.
Treat the agent like any other telemetry producer: give it a named pipeline in the Collector, with its own filters and limits. If it misbehaves, you can throttle or disable its data without touching anything else.
PPerformance and Overhead: What Does It Really Cost?
eBPF is often described as low overhead. That is true in design, but the actual cost depends on how many events your machines produce and how much work the agent does with each one.
Where the Cost Comes From
| Cost source | Who pays | What drives it |
|---|---|---|
| Probe execution | The application’s own request path, in microseconds | Number of events hit, and how much each probe does |
| Copying events out | Kernel and agent | Bytes captured per event and event rate |
| Parsing and span building | The agent’s CPU | Protocol complexity and request rate |
| Metadata lookups | The agent and the Kubernetes API | Pod churn and caching quality |
| Export | The agent and the network | Spans and metrics produced, and batching |
| Memory for maps and buffers | Kernel memory on the node | Number of tracked connections and buffer sizes |
A Back-of-the-Envelope Estimate
The following numbers are illustrative assumptions, not measurements. Suppose a node serves 5,000 requests per second, and each request produces about six probe events: one for the inbound read, one for the outbound response write, and two each for a downstream call. That is 30,000 events per second. If a probe takes about one microsecond to run, the kernel-side cost is 30 milliseconds of CPU per second, roughly three percent of one core spread across the application’s own request paths.
Now consider the agent. If parsing and span building take around ten microseconds per event, the agent needs about 300 milliseconds of CPU per second, close to a third of one core. The lesson is that the user-space half usually costs more than the kernel half. Efficient parsing, caching and batching matter more than shaving nanoseconds from probes.
A toll booth adds a few seconds to each car’s journey, which is almost nothing for one car. The real cost shows up at the accounting office that processes millions of tickets afterward. Probes are the toll booth; the agent is the accounting office.
Sharing Across Processes
An in-process agent runs inside every service, with its own memory and CPU use, multiplied by the number of service instances. An eBPF agent runs once per node, so its fixed costs are paid once for all the pods on the machine. For nodes that host dozens of small services, this amortization is a real advantage. For a node running a single large service, the benefit is smaller.
How to Measure It Yourself
Choose matched nodes
Pick two groups of similar nodes carrying similar traffic, one with the agent and one without.
Compare the application
Measure request latency at high percentiles, throughput and CPU of the application on both groups over several days, including peak hours.
Measure the agent
Record the agent’s CPU, memory, dropped events and export volume. Watch drop counters especially closely.
Decide with data
If differences in the application are within noise and the agent’s footprint fits your budget, widen the rollout. If not, tune scope before going further.
Benchmarks run on idle test machines often flatter the tool. Always test during production-like load, and include the busiest hour of your busiest day, because event rates and ring-buffer pressure peak when you most need the data.
QTuning an eBPF Agent at Scale
A handful of settings decide whether an agent is quiet and useful or noisy and costly across hundreds of nodes.
Narrow the Scope
The cheapest event is one that is never observed. Restrict the agent to the namespaces, ports or workloads you care about. Exclude infrastructure noise and test traffic. Disable protocols you do not use, so parsers for them are not even loaded. Scope is the biggest single lever, and the easiest to adjust gradually.
Mind the Ring Buffer
Remember that the ring buffer is a fixed-size shared area. If events arrive faster than the agent drains them, new events are dropped. Suppose each event carries 300 bytes and the node produces 30,000 events per second. That is about nine megabytes per second flowing into the buffer. A buffer of eight mebibytes would hold less than one second of events. If the agent stalls for longer than that, because of CPU starvation or a garbage-collection pause, data is lost. Larger buffers buy time, at the cost of kernel memory, and sufficient CPU for the agent buys more.
A Table of Levers
| Lever | Effect | Risk |
|---|---|---|
| Scope by namespace, port or label | Fewer events, lower cost | Blind spots if scope is too narrow |
| Disable unneeded protocols | Less parsing, smaller attack surface | Missing data when new protocols appear |
| Limit bytes captured per event | Smaller buffers, less sensitive data exposure | Cut-off headers or truncated queries |
| Larger ring buffer | Absorbs bursts and agent pauses | More pinned kernel memory |
| Agent CPU reservation | Keeps up with event rates | Higher fixed cost per node |
| Sampling of traces | Lower span volume | Rare problems may be missed |
| Route templating | Controls metric cardinality | Wrong templates merge distinct endpoints |
| Export batch settings | Fewer, larger requests; less CPU | More delay before data appears |
Tuning a security camera system means deciding which doors to watch, how much storage to keep and how often to check recordings. Watching every door in high resolution and keeping everything forever is simple but expensive. Choosing the important doors and a sensible resolution gives most of the value for a fraction of the cost.
Treat Rollout as an Experiment
Start with one cluster and one namespace. Record three baseline numbers: agent CPU, drop counts and number of distinct route values. Expand in steps, such as ten percent of nodes, then half, then all, and check the same numbers at each step. Keep a documented way to disable the agent quickly, such as a configuration flag, so that a problem found at three in the morning can be contained in minutes.
Create a dashboard for the agents themselves: events seen, events dropped, spans exported, export errors, CPU and memory. An observability tool that is not itself observed is the most common surprise during incidents.
RContinuous Profiling With eBPF
Traces tell you which request was slow. Profiles tell you which lines of code were busy. eBPF makes the second kind of data cheap enough to collect all the time.
What a Profile Is
A CPU profile is a statistical picture of where a program spends its processing time. A profiler periodically interrupts the processor, notes the call stack, the chain of functions currently running, and moves on. After thousands of samples, the functions that appear most often are the ones using the most CPU. Results are usually drawn as a flame graph, where wider boxes mean more time.
Imagine photographing a busy office every few seconds for a week and counting how many photos show each person at the printer, the meeting room or the coffee machine. You do not need to watch constantly. After enough photos, the counts reveal where time really goes. Profiling is that office photography for code.
Why eBPF Suits Profiling
A performance event hook lets an eBPF program run at a fixed rate on each CPU, for example some tens of times per second. At each tick, the program records the current stack. Because it runs in the kernel and sees every process, a single agent per machine profiles all programs at once, without restarting them or adding special flags. At such a low sampling rate, overhead stays small enough to leave switched on permanently, which is what makes continuous profiling practical.
The hard part is reading stacks. Native programs may omit the information that makes unwinding easy. Interpreted and just-in-time-compiled languages keep their own structures. OpenTelemetry’s eBPF profiler contains logic to unwind native code and several popular language runtimes directly from eBPF, which is a large part of its value.
Cost of the Data
Take a machine with 64 CPU cores and a sampling rate of 20 samples per second per core. That is 1,280 stacks per second. Many repeat the same few paths, so the agent aggregates them into counts before export, and the data is small compared with traces. Turning raw addresses into readable function names, called symbolization, is often done later on a server, using debug information, to keep the node light.
What Questions It Answers
| Question | What the profile shows |
|---|---|
| Why did CPU usage double after the last release? | Which functions grew wider in the flame graph between versions |
| Which service is the best target for cost savings? | The services and functions that consume the most processor time across the fleet |
| Is the time in our code or in a library? | Stacks split by application code, runtime and system libraries |
| What is burning CPU during incidents? | A stack-level view for the exact time window, available without reproducing the issue |
Profiling is a newer OpenTelemetry signal, and its data model and protocol support are still being finalized. Links between a profile and a particular trace are also still developing. Plan for change, and test your chosen backend’s support before relying on it.
SNetwork Observability With eBPF
Sometimes the question is not what the code did but how the network behaved between services. eBPF sits right where that information lives.
What the Kernel Knows About Connections
The kernel tracks every network connection: who opened it, to whom, how many bytes moved, how many packets were lost and retransmitted, and how long round trips take. Probes and socket hooks can sample these facts without touching the application. Combined with Kubernetes metadata, they turn raw addresses into statements like “the checkout pods sent 40 gigabytes to the inventory pods yesterday, with a rising rate of retransmissions”.
Typical Network Signals
| Signal | Meaning | Why you care |
|---|---|---|
| Bytes and packets per workload pair | How much traffic flows between two services | Finds chatty services and capacity limits |
| Retransmissions | Packets that had to be resent | Early sign of congestion or faulty links |
| Round-trip time | Network delay between endpoints | Separates network slowness from application slowness |
| Connection counts and resets | How often connections open, close or fail | Exposes connection storms and misconfigured timeouts |
| DNS lookup time and failures | How quickly names are resolved | A frequent hidden cause of intermittent latency |
| Cross-zone or cross-region traffic | Data crossing cost boundaries | Cloud providers often bill for it |
Road traffic counters tell a city planner how many vehicles pass between neighbourhoods, how often they stop and how long trips take, without opening a single truck to see its cargo. Network observability is the road counter. It is superb for questions about volume and delay, and says nothing about contents.
Service Maps From Connections
Because each connection has a source and a destination, you can draw a map of which services talk to which, without any trace headers. This map is built from facts, not from declared configuration, so it reveals surprising dependencies such as a forgotten service still being called every minute. Even where full traces cannot be connected, these maps remain accurate.
Beware of Flow Cardinality
A raw list of every connection includes ephemeral ports and short-lived pod addresses, so its cardinality, the number of unique combinations, is enormous. Good network tools aggregate by workload, namespace or zone before export. Keep the unit of reporting at the level that people reason about, such as “service A to service B”, and drop port and address details from long-term metrics. Otherwise, the backend cost grows with every pod restart.
Use network metrics to separate causes. When a service is slow, round-trip time and retransmissions tell you quickly whether the network is to blame, which can save a whole incident’s worth of guessing.
TSecurity, Privacy and Trust
An agent that can see every request on a machine is powerful, and anything powerful must be handled with care.
What the Agent Can See
At the protocol level, an eBPF agent can read the beginning of every message crossing the machine. That includes URLs, headers and sometimes parts of bodies. Those may contain things that should never be stored: access tokens in headers, personal identifiers in paths, secrets in query strings, or fragments of database statements with real values. Even if the agent never exports these fields, it has had access to them, which matters for risk assessment and compliance.
The front desk of a building sees every visitor’s face and name. That is useful, and also sensitive. A good building limits who works at the desk, what is written in the logbook and how long the logbook is kept. An eBPF agent is the front desk of your machines and deserves the same discipline.
Principles for Safe Use
- Capture the minimum. Limit how many bytes per message are captured and which fields are kept. Do not capture bodies unless you have a specific, reviewed need.
- Redact early. Remove or mask sensitive values in the agent, and again in the Collector, before anything is stored.
- Exclude sensitive workloads. Keep payment, health or identity systems out of scope until their owners have reviewed what will be observed.
- Get consent from owners. In shared clusters, tell teams what is being observed and how to opt out or adjust.
- Minimize privileges. Grant exactly the capabilities the tool documents, not a blanket privileged mode.
The Agent Is a Valuable Target
Because the agent has elevated permissions on every node, compromising it would be attractive to an attacker. Reduce that risk like you would for any privileged component: pull images only from trusted registries and verify signatures where available, pin versions, restrict who can modify the DaemonSet, keep the agent’s own network access narrow, and review its permissions after each upgrade. Log changes to its configuration, so that unusual changes are visible.
Verified Is Not the Same as Harmless
The kernel’s verifier guarantees that an eBPF program cannot crash the system or read memory outside its allowed areas. It does not guarantee that the program’s purpose is acceptable. A perfectly verified program can still read sensitive data it is allowed to see. Trust therefore comes from knowing who wrote the program, reviewing its source or its vendor’s attestations, and controlling who can load new programs on your nodes.
| Risk | Mitigation |
|---|---|
| Sensitive values captured from URLs or headers | Limit captured bytes; redact in agent and Collector; test with fake sensitive data |
| Agent compromise | Trusted images, restricted deployment rights, minimal capabilities, configuration audit |
| Overbroad visibility in shared clusters | Scope by namespace; publish what is observed; offer opt-out for sensitive teams |
| Unreviewed programs loaded on nodes | Restrict who holds the loading capabilities; alert on unexpected loaded programs |
| Retention of sensitive data in the backend | Short retention for risky fields; access controls on telemetry stores |
UWhat eBPF Cannot See
Honest tools have honest limits. Knowing them in advance prevents disappointment and wrong decisions.
The Business Meaning Gap
An eBPF agent sees that a request to /checkout took 230 milliseconds and returned a success code. It does not see that the customer was on a premium plan, that the cart held eleven items or that a discount rule fired. Those facts live inside the application’s memory and logic, and never cross a hook in a form the agent can interpret. Anything that requires knowing the meaning of the data, not just its shape, needs in-code instrumentation.
Errors That Look Like Successes
Many applications return a normal success status even when the work failed, placing the real error inside the body of the response. GraphQL services are a classic example. A protocol-level agent that reads only status codes will report these as healthy requests. If your error signals live in message bodies, you need the application to expose them as attributes or metrics.
A Table of Gaps
| You need | What eBPF gives | Better source |
|---|---|---|
| Customer, tenant or plan details on spans | Nothing, unless visible in headers you allow | SDK attributes set in code |
| Internal steps inside one service | Little, except for specific supported functions | Manual spans or an in-process agent |
| Application-level errors hidden in bodies | Only status codes | Custom metrics or span status set in code |
| Structured log content | Not its strength | Application logging with trace identifiers |
| Serverless function invocations | Not available where nodes are hidden | Platform integration or SDK |
| Windows servers | Limited or none, depending on tool | In-process agents |
| Calls over unsupported encryption libraries | Encrypted bytes only | In-process instrumentation or a proxy that terminates encryption |
A traffic camera at a junction can tell you how many cars passed, how fast and in which direction. It cannot tell you that one of them was an ambulance rushing to an emergency or that the driver was lost. For those facts you need someone inside the vehicle. In-code instrumentation is the person inside the vehicle.
Service Meshes and Double Counting
In clusters that use a service mesh, a sidecar proxy sits beside each application and handles its network traffic. An eBPF agent on the same node can see both the application’s traffic to the proxy and the proxy’s traffic to the network, producing two sets of spans for one call. Decide which layer is the source of truth for each service, and filter the other, so that you do not double the volume or the apparent request count.
The easiest way to disappoint stakeholders is to promise “full tracing with zero effort” and then discover that important business attributes are absent. Describe the result as “broad, standard coverage on day one, with depth added where it matters”.
VeBPF Versus SDKs, Agents and Service Meshes
No approach wins every comparison. The table below sets them side by side so you can see the trade-offs at a glance.
| Criterion | Manual SDK | In-process agent | eBPF-based | Service mesh proxy |
|---|---|---|---|---|
| Setup effort | High, per service | Medium, per service | Low, per node | Medium, per mesh |
| Code changes | Yes | No | No | No |
| Business-level detail | Excellent | Limited | None to limited | None |
| Library-level detail | Only what you write | Very good | Moderate | None |
| Language independence | No | No | Largely yes | Yes |
| Where overhead lands | In each process | In each process | In kernel and one agent per node | In a proxy per pod |
| Privileges needed | None special | Usually none special | Elevated on nodes | Mesh control rights |
| Trace propagation | Complete | Complete | Varies by tool and kernel | Needs application help |
| Works on locked-down platforms | Yes | Mostly | Often no | Depends |
Strengths of eBPF-based instrumentation
- No code changes, rebuilds or restarts
- One approach for many languages
- Coverage for programs you cannot modify
- Fast time to first useful data
- Fixed costs shared per node
- Also enables profiling and network visibility
Weaknesses of eBPF-based instrumentation
- Little business context or custom detail
- Trace propagation is harder and varies
- Needs recent Linux kernels and elevated privileges
- Depends on symbols and library versions for deep probes
- Not available on many managed or serverless platforms
- Projects are young and still evolving
Choosing among these approaches is like choosing tools for home repairs. A multi-tool gets many small jobs done quickly with no setup. A specialist tool does one task superbly but takes time to set up. Professionals own both, and use each where it fits.
Questions That Decide the Choice
- Can you change and redeploy the code? If not, eBPF may be your only practical option.
- Do you need business detail? If so, plan for SDK instrumentation on key services.
- Can your platform host privileged node agents? If not, in-process agents are the realistic path.
- How many languages do you run? The more there are, the more attractive a single node-level approach becomes.
- How important are complete end-to-end traces? If very, test propagation carefully with your actual traffic.
WCombining Approaches: A Floor and a Ceiling
The most successful teams treat eBPF as the floor of their observability and SDKs as the ceiling.
The Floor
The floor is the minimum level of visibility every service must have, no matter who owns it or what language it uses: request rate, errors, duration, dependencies and basic resource use. eBPF is ideal for establishing this floor quickly, because it requires nothing from service teams. Once it is in place, nobody is invisible, and unknown services stop being a surprise during incidents.
The Ceiling
The ceiling is the richest view you can get, built by people who know the code. Critical services receive manual spans around key business steps, custom metrics for things like orders and payments, and attributes that identify customers, plans and features. This costs effort, so it is applied where the value is highest.
flowchart TD
A["Can you change and redeploy the code?"] -->|"No"| B["Use eBPF as the main source"]
A -->|"Yes"| C{"Need business detail
or custom spans?"}
C -->|"Yes"| D["Use SDK or in-process agent
and add eBPF for gaps"]
C -->|"No"| E{"Can the platform run
privileged node agents?"}
E -->|"Yes"| F["Use eBPF for fast broad coverage"]
E -->|"No"| G["Use in-process agents"]
Avoid Duplicates
When a service has both SDK instrumentation and an eBPF agent watching it, both may create a server span for the same request. The result is doubled counts and confusing traces. There are three ways to avoid it. Exclude SDK-instrumented services from the eBPF agent’s scope. Filter one source out in the Collector for those services. Or use an agent that detects existing instrumentation and steps back, if your chosen tool offers that behaviour. Whichever you choose, test with one service and compare counts against your load generator.
One Vocabulary for Both
Combined data is only useful if both sources speak the same language. Agree on one edition of the semantic conventions, one rule for service names, and one set of resource attributes. Use the Collector to normalize any differences. Dashboards then work across the whole fleet, and a service can graduate from eBPF-only to SDK instrumentation without breaking charts.
A Typical Journey
Week one
Deploy the eBPF agent to a pilot cluster. Verify names, cardinality and overhead.
Month one
Roll out to all clusters that meet kernel requirements. Build baseline dashboards and alerts on request rate, errors and duration.
Quarter one
Choose the ten most critical services and add SDK spans and business attributes. Configure the eBPF agent to step aside for them.
Ongoing
Promote more services to the ceiling as value is proven, and keep eBPF as the safety net for everything else.
XFailure Modes and Troubleshooting
When data is missing or strange, a short list of usual causes explains most cases.
| Symptom | Likely cause | What to do |
|---|---|---|
| Agent fails to start on some nodes | Old kernel, missing type information, or blocked permissions | Check kernel version and BTF; review security policy; compare against a working node |
| No data for one service | The service is not discovered, binary has no symbols, or it uses an unsupported library | Check scope rules; test with a sample binary; read supported-library lists |
| Gaps in spans during peak load | Ring buffer drops because the agent cannot keep up | Increase agent CPU; enlarge buffer; reduce scope or captured bytes |
| Doubled request counts | Both SDK and eBPF instrument the same service, or a mesh proxy is also observed | Pick one source of truth per service and filter the other |
| Traces broken into pieces | No header propagation between services | Make applications forward trace headers or use a tool and kernel that support injection |
| Metric costs jump | Raw URL paths became attributes | Enable route templating and add Collector normalization |
| Services named after programs | Identity enrichment failing | Fix labels and naming rules; check Kubernetes permissions |
| Instrumentation stopped after an application upgrade | Function names or layouts changed, so probes no longer match | Update the agent; check release notes for supported versions |
| Agent restarts or runs out of memory | Too many tracked connections or insufficient limits | Raise limits; narrow scope; check connection churn |
| Kubernetes API load spikes | Many agents watching broad sets of objects | Use caching, per-node watches or central enrichment |
A Debugging Routine
Check the agent’s health
Is it running on the node? Look at its logs for load errors, and at its own metrics for drops and export failures.
Confirm the platform basics
Verify kernel version, type information and granted capabilities on the affected node.
Confirm discovery
Does the agent list the target process as instrumented? If not, adjust scope rules.
Check protocol and encryption support
Is the traffic a protocol the tool parses, and does the application use an encryption library it can hook?
Compare with a known good
Run a simple test service, such as a basic web server, on the same node. If it works, the issue lies with the target application.
Report upstream
Gather the kernel version, tool version, application language and library versions and open an issue with the project, because many gaps are fixed quickly once reported.
Keep a small, boring test service in every cluster. When something looks wrong, its presence or absence in your dashboards separates “the agent is broken” from “this application is special”.
YBest Practices and Anti-Patterns
Teams that adopt eBPF instrumentation successfully share a few habits. Teams that struggle share a few mistakes.
Habits That Work
Pilot first
Begin with one cluster and one namespace, and measure overhead, drops and cardinality before expanding.
Watch only what you need
Limit by namespace, port and protocol. Smaller scope means lower cost and less sensitive data seen.
Fix names early
Agree on service naming rules, and verify that no services appear under generic program names.
Send through a Collector
Apply filtering, redaction, normalization and sampling in one place for all sources.
Grant minimal privileges
Use the documented capability list, not a blanket privileged mode, and restrict who can change the agent.
Add SDKs where value is high
Use eBPF for the floor and manual instrumentation for critical business paths.
Monitor the agents
Track drops, CPU, memory and export errors on a dashboard owned by the platform team.
Track versions and kernels
Maintain a compatibility matrix and test agent upgrades on canary nodes first.
Mistakes That Teach
What people do
They deploy the agent cluster-wide with default settings on the first day, watching every namespace and every protocol.
Why it hurts
Unexpected cardinality, noisy infrastructure traffic and agent resource pressure appear everywhere at once. Teams lose trust before the value is clear, and the sensitive-data review has not happened.
Do this instead
Roll out in stages with measured checkpoints, and review scope, redaction and overhead at each one.
What people do
They run the agent in a fully privileged mode with broad host access because it was the fastest way to get it working.
Why it hurts
A compromise of that one component gives an attacker control of every node. Security reviews later force a disruptive redesign.
Do this instead
Start from the documented minimal capabilities, test them in staging, and restrict deployment rights from day one.
What people do
They add eBPF on top of services that already have SDK instrumentation and never check for duplicate spans.
Why it hurts
Request counts double, traces show repeated nodes and alerts based on rates fire incorrectly. Costs rise with no new information.
Do this instead
Choose a single source of truth for each service, exclude or filter the other, and verify counts against a known load.
ZReal-World Patterns
The same technology gets used in quite different ways depending on the organization. These illustrative scenarios show the range.
A legacy system nobody wants to touch
A decade-old billing engine written in C++ runs reliably and has no tests that anyone trusts. Adding an SDK would require rebuilding it, which management will not approve. An eBPF agent on the node gives request rates, error rates and latency for the engine’s network calls, and the team finally sees how its slowest calls affect downstream services, all without a single change to the binary.
A platform team establishing a baseline
A platform team supports 300 services in five languages. Rather than coordinate five instrumentation projects, it deploys one agent per node and delivers request metrics and service maps to every team within weeks. It then offers SDK guidance and templates for teams that want richer detail, and measures adoption of both layers.
Vendor-supplied binaries
A company runs third-party software, including a message broker and a commercial gateway, delivered without source. Neither can accept an SDK. The eBPF agent observes them at the protocol level, which reveals that one broker consumer is responsible for most of the end-to-end delay in a nightly job.
An unexplained latency incident
During an incident, SDK traces show a slow service but nothing inside it explains the delay. Network metrics from the same agent reveal retransmissions and long DNS lookups on a single node pool. Profiling data shows the service itself is idle. The cause is a network problem, found in minutes instead of hours.
A cost-reduction campaign
A finance-driven effort asks where compute money goes. Continuous profiles, collected across the fleet at low overhead, show that a few library functions in widely used services consume a surprising share of CPU. Fixing them lets the company shrink several node pools.
Industry Context
eBPF’s reputation rests on years of production use. Large operators have publicly described using it for high-speed load balancing and for filtering attack traffic, and well-known performance engineers helped popularize eBPF tracing tools for finding bottlenecks on busy servers. OpenTelemetry’s contribution is to turn that raw capability into standard telemetry that any compatible tool can consume.
AAA Maturity Ladder for eBPF Adoption
Adoption grows in stages. Knowing which stage you are in tells you what to do next, and what to avoid doing too early.
| Level | What it looks like | Next move |
|---|---|---|
| 1. Exploring | A small pilot on a test cluster; a few people understand how it works | Measure overhead, check kernel compatibility and review data captured |
| 2. Baseline | The agent covers most clusters; request metrics and service maps exist for all services | Fix naming, cardinality and duplicates; share dashboards with teams |
| 3. Combined | Critical services add SDK detail; sources share naming and conventions; Collector policy applies to all | Automate checks and add profiling or network signals where valuable |
| 4. Operated | Agents have dashboards, alerts and upgrade routines; security reviews are routine; teams self-serve scope changes | Refine, contribute fixes upstream and retire redundant tooling |
Learning to drive goes from practising in an empty car park, to driving on quiet roads, to motorways, to driving in storms. Skipping straight to storms is dangerous. Each stage prepares you for the next, and a stage is complete when the skills feel boring.
Do Not Climb Faster Than Your Platform
A company whose clusters run old kernels or locked-down managed services may stay at level one for a while, until platform upgrades catch up. That is a reasonable outcome. The right move is to use in-process agents where eBPF cannot run, and plan the platform changes that would make eBPF possible, instead of forcing a fragile deployment.
ABA Step-by-Step Adoption Playbook
When you are ready to try this in your own environment, follow this order. Each step reduces risk for the next.
Inventory the fleet
List services by language, platform and criticality. Note which clusters use which kernels and security policies.
Choose the tool
Read the current documentation of the candidate projects. Check supported protocols, kernels, stability notes and how each handles trace propagation.
Brief security early
Share capabilities needed, data visible, retention plans and disable procedures. Agree on redaction and excluded workloads.
Pilot in staging
Deploy to a cluster with production-like policies. Run realistic load and compare request counts with your load generator.
Set naming and pipeline rules
Fix service naming, route templating and Collector processors. Decide how to avoid duplicates with existing SDK data.
Measure overhead
Compare matched node groups with and without the agent through at least one peak period. Check drops and agent resources.
Roll out in stages
Expand to a small share of production nodes, then half, then all, checking the same measurements at each step and keeping a quick disable switch.
Build the ceiling and operate
Add SDK detail to critical services, publish dashboards, monitor the agents and revisit the setup after kernel, tool or application upgrades.
Change one thing at a time and keep an easy way back. With a technology that works at the kernel level, the most valuable safety feature is a switch that turns it off in seconds.
ACA Plain-Language Glossary
A quick reference for the terms used in this guide.
| Term | Plain meaning |
|---|---|
| eBPF | A Linux technology that runs small, safety-checked programs inside the kernel in response to system events. |
| Kernel | The core of the operating system that manages memory, processes, disks and networks. |
| User space | The normal area of the system where applications and agents run, outside the kernel. |
| Verifier | The kernel’s checker that proves an eBPF program is safe before it can run. |
| Hook | An event in the system, such as a function call or network read, that an eBPF program can be attached to. |
| Probe | An eBPF program attached to a hook to watch an event. |
| Map | A shared table in kernel memory used to store state and exchange data. |
| Ring buffer | A fixed-size circular area through which the kernel passes events to a user-space reader. |
| BTF and CO-RE | Kernel type information and a technique that let one eBPF build adapt to many kernel versions. |
| Capability | A small piece of administrator power that can be granted to a program on its own. |
| Instrumentation | The act of making a program report on itself through telemetry. |
| SDK | The OpenTelemetry library that lives inside an application and creates telemetry. |
| Auto-instrumentation | Adding telemetry to a program without changing its source code. |
| Trace and span | A trace is the journey of one request; each timed step in it is a span. |
| Context propagation | Passing a trace identifier from one service to the next so their spans join the same trace. |
| RED metrics | Rate, errors and duration: the basic health numbers for a service. |
| OTLP | The OpenTelemetry Protocol used to send telemetry between components. |
| Collector | A program that receives, processes and forwards telemetry. |
| DaemonSet | A Kubernetes object that runs one copy of a program on every node. |
| Cardinality | The number of unique value combinations of an attribute, which drives storage cost. |
| Profile and flame graph | A statistical picture of where code spends CPU time, drawn as boxes whose width shows time. |
| Symbolization | Turning raw memory addresses in a profile into readable function names. |
ADFrequently Asked Questions
Short answers to the questions that come up most often.
AESummary and Key Takeaways
eBPF-based instrumentation lets you observe programs from the operating system’s side, producing standard OpenTelemetry data without touching application code.
You began with a company that could not instrument all its code, and learned how eBPF offers a safe, language-independent way to watch programs from the kernel. You saw how probes and a user-space agent turn low-level events into spans, metrics, profiles and network signals, how the OpenTelemetry projects package those ideas, and where the approach struggles, especially with context propagation, business meaning, privileges and platform limits.
Key Takeaways
- eBPF runs small, verified programs in the kernel. The verifier makes them safe, and hooks decide which events they can watch.
- The work is split in two. Probes capture raw facts cheaply in the kernel; a user-space agent parses them, builds standard spans and metrics, adds identity and exports through OTLP.
- It trades depth for reach. You get broad, language-independent, request-level coverage with no code changes, but little business context or internal detail.
- Context propagation is the hardest part. Reading trace headers is easy, writing them is not, so test whether your traces connect across services before promising end-to-end views.
- Requirements and privileges are real. Plan for recent Linux kernels, type information, minimal capabilities, security review and platforms that may not allow node agents.
- Measure cost and tune scope. The user-space agent usually costs more than the probes, so narrow scope, size buffers, normalize routes and watch the agents themselves.
- Use eBPF as the floor and SDKs as the ceiling. Cover everything quickly, add detail where it pays, avoid duplicate spans and keep one shared vocabulary for names.