eBPF-Based Observability for Wasm Workloads
Why WebAssembly workloads are invisible to classic APM, what eBPF can and cannot see at the syscall boundary, how to trace host calls, and how to correlate kernel events with OpenTelemetry.
Published on • November 15, 2026
AI Assistant

WebAssembly was designed to be opaque. A Wasm module executes on a virtual stack machine inside linear memory, with no direct syscall path — every byte it reads or writes travels through host functions that the runtime resolves. That is excellent for security and terrible for observability: your APM agent finds no HTTP client library to instrument, your strace sees only the host process’s file descriptors, and your flame graphs show a single blob named wasm32-wasi-release.wasm. eBPF offers a way out — but only if you understand exactly where its visibility ends at the sandbox boundary.
Why Wasm Defeats Classic Observability
Traditional observability stacks assume three things a Wasm workload breaks:
- Instrumentation lives in-process. OpenTelemetry SDKs are language- and runtime-specific:
opentelemetry-rustcompiles to native code and links libc. Inside a Wasm sandbox there is no libc, no threads (historically), and no sockets — the SDK cannot even reach an OTLP endpoint without a host shim. - Threads and symbols are visible. A profiler sampling stack frames relies on frame pointers, DWARF unwind info, and recognizable symbols. A wasmtime host thread alternates between native runtime code and JIT-compiled Wasm machine code with no unwind metadata; sampled stacks collapse into
wasmtime::executeand nothing below it. - One process equals one logical unit. Serverless Wasm platforms multiplex many tenants’ instances into shared runtimes (SpinKube, wasmCloud, Cloudflare Workers’ model). PID-level metrics attribute everything to the runtime container, so per-instance latency and error budgets are unmeasurable from the outside.
The result: a fleet of Wasm services that is fast to cold-start and impossible to debug with the tools you already run.
What eBPF Can Still See
eBPF does not need to be inside the sandbox — it sits in the kernel, observing the host process (wasmtime, WasmEdge, lunatic, the Spin executor). From there it sees everything the sandbox does to the outside world:
| Event source | What it reveals about the Wasm workload |
|---|---|
sys_enter_* tracepoints | File and socket syscalls issued by the host on behalf of guests |
kprobes on vfs_*, tcp_* | Actual file paths opened, bytes read/written, connection targets |
| uprobes on runtime symbols | Entry/exit of runtime functions — JIT compilation, instance execution, GC/alloc |
| WASI host-function probes | fd_write, path_open, clock_time_get calls with arguments |
| sched/cgroup tracepoints | Per-instance CPU time when the runtime isolates instances per cgroup |
LSM hooks (security_file_open) | Enforcement-grade file access records with process identity |
A concrete example — instrumenting the WASI fd_write host function that every guest console print funnels through, without rebuilding the runtime:
bpftrace -e 'uprobe:/usr/local/bin/wasmtime:wasi_snapshot_preview1::fd_write
/str(args->fd) == "1"/ { @writes[pid] = count(); }'
Symbol names vary by runtime and version (wasmtime’s component-model path uses different export names than wasi_snapshot_preview1), but the principle is stable: the host runtime’s own functions are ordinary user-space symbols, and uprobes attach to them for free. More robust deployments put these probes in a compiled Cilium/Tetragon-style agent or Pixie-style collector rather than ad-hoc bpftrace, so probe definitions ship as versioned artifacts.
What eBPF Cannot See
The honesty section matters, because the limits are structural:
- No guest-internal control flow. eBPF cannot step into JIT-compiled Wasm code, name guest functions, or attribute a slow span to a function inside the module. Wasm JIT pages are executable memory with no symbols; uprobe sites inside them do not exist unless the runtime cooperates.
- No per-instance attribution by default. All instances sharing one host process collapse into one PID. Attribution requires runtime cooperation — cgroup-per-instance, per-instance ports, or runtime-emitted telemetry.
- No guest memory state. Linear memory contents are invisible; you cannot inspect a guest’s heap or an in-sandbox queue from kernel probes.
- No visibility into host calls that stay in-process. If two instances communicate through an in-memory channel inside the runtime, no syscall occurs and no kernel hook fires.
The dividing line is crisp: eBPF observes the sandbox’s I/O; only the runtime can observe the sandbox’s computation.
Correlating with OpenTelemetry
The practical architecture is a fusion, not a replacement. eBPF supplies instrumentation-free system-level facts; OpenTelemetry supplies application-level meaning:
- An eBPF agent (Tetragon-style process/file/socket events, or a dedicated Wasm observer) emits events stamped with cgroup, PID, and pod metadata.
- A correlation service maps cgroup/PID → Kubernetes pod → Wasm instance ID (via annotations the platform already knows, or container labels on SpinKube’s
ContainerdSpinShim). - Runtime-emitted OTel spans (the Spin SDK and
spin-sdktelemetry hooks can export traces to an OTLP collector inside the pod) join the same trace via sharedservice.nameand trace context propagated through the host-function boundary.
The eBPF side then fills the gaps OTel cannot: cold-start duration measured from execve to first WASI call, host-side I/O latency the guest only experiences as a stall, and — in incidents where developers forgot instrumentation entirely — at least a complete record of which endpoints and files the workload touched.
The correlation contract itself is just resource attributes. Whatever your platform knows about instance placement gets stamped on both streams:
# OTel resource emitted by the Spin SDK inside the pod
service:
name: "invoice-renderer"
namespace: "payments"
spin:
component: "render"
instance: "550e8400-e29b-41d4-a716-446655440000"
k8s:
pod.name: "invoice-renderer-7d9f8b6c5-x2lqp"
container.name: "spin"
An eBPF-derived event for the same pod carries k8s.pod.name and k8s.container.name from cgroup metadata; join on those two fields and a kernel-level openat on a slow NFS mount becomes a child span of the guest’s render operation instead of an unexplained stall. The discipline that makes this work is boring and essential: one attribute naming scheme, agreed once, emitted by both sides.
Research Directions
The active research line is exactly this hybrid. A 2025 IEEE ITNAC paper by Hu, Komu, Simanainen, and Kjällman at Ericsson Research prototype an eBPF agent observing serverless Wasm workloads in Kubernetes running on SpinKube, explicitly as an alternative to mandatory OpenTelemetry instrumentation: their measured overhead was 17.8% average latency and 8% maximum CPU — real, but in the range operators accept for always-on telemetry. Related work pushes in both directions: runtime-integrated OTel semantics for the Wasm component model, eBPF-to-Wasm bridges (wasm-bpf) that make the observer itself portable, and uprobe-based JIT-symbol reconstruction for runtimes that expose symbolizer hooks.
Open problems worth watching: stable instance identity for multiplexed serverless runtimes, component-model-aware span boundaries (a component call is not a syscall), and standardizing a minimal “Wasm workload telemetry” profile so that eBPF agents and runtime SDKs emit compatible correlation keys instead of each platform inventing its own.
References
- IEEE Xplore (cited paper): https://ieeexplore.ieee.org/document/11340184
- Hu et al., “eBPF-based Observability for Serverless WebAssembly Workloads,” ITNAC 2025 (DOI: 10.1109/ITNAC66378.2025.11302671)
- SpinKube — running Spin applications on Kubernetes: https://spinkube.dev/
- OpenTelemetry specification (traces, resource attributes): https://opentelemetry.io/docs/specification/
- wasm-bpf: bridging WebAssembly and eBPF: https://eunomia.dev/blog/2023/02/11/wasm-bpf-bridging-webassembly-and-ebpf-for-kernel-programmability
- wasmtime WASI documentation: https://docs.wasmtime.dev/wasi/