Skip to content
Blog

eBPF vs WebAssembly as Offloading Mechanisms

A head-to-head comparison of eBPF and WebAssembly as offload targets — programmability, safety verification, performance, portability, and toolchains — and where each one actually wins.

Published on • November 15, 2026

AI Assistant

Two sandboxed bytecodes dominate the “run my code somewhere else” conversation: eBPF, which executes inside the Linux kernel, and WebAssembly, which executes in a user-space runtime. Both are compiled from C, Rust, or Go; both are verified before execution; both JIT-compile to native code. It is tempting to treat them as interchangeable — they are not. A 2021 evaluation by Huang and Paradies at the German Aerospace Center (DLR) compared them head-to-head for computational-storage offloading, and their conclusions generalize well beyond storage: eBPF wins when the data plane is the target, WebAssembly wins when portability and language freedom matter, and the toolchains are converging rather than competing.

Two Bytecodes, Two Worlds

The architectural differences are load-bearing:

DimensioneBPFWebAssembly
Stack modelRegister-based (11 64-bit registers)Stack-based
Execution environmentLinux kernel onlyAny runtime (browsers, wasmtime, WasmEdge, Wasmtime)
Isolation modelLoad-time verifier proofSpec-level linear memory + sandbox
System interfaceWhitelisted BPF helpersWASI (preview1/2)
Source languagesC, Rust (Aya), limited othersC/C++, Rust, Go, Python, JS, many more
Endianness of binariesHost-endian (bpf/bpfeb/bpfel)Always little-endian
StandardizationKernel ABI + de facto conventionsW3C standard

eBPF’s constraint set is aggressive: no libc, no dynamic allocation in-kernel (without bpf_allocator-era extensions), no floating-point in most contexts, calls restricted to registered helpers. WebAssembly’s is comparatively generous: a linear memory region, imports resolved by the host, and a WASI syscall shim that gives you fd_read, path_open, and clocks.

Safety: Proof vs. Isolation

This is the most cited difference and the least symmetric.

WebAssembly’s safety is specified at the language level: a module can only touch its own linear memory, has no file system access unless the host grants it, and cannot escape its sandbox without a runtime bug. Worst case, a buggy module corrupts its own heap or spins forever — runtimes like wasmtime offer fuel-based metering to cap that. The guarantees do not depend on any particular implementation.

eBPF’s safety is proved, not sandboxed. The verifier statically analyzes every program at load time to prove bounded loops, in-bounds memory access, and type-correct register usage. That is powerful — it lets a program run in the same address space as the kernel with zero runtime isolation overhead — but it is also brittle in practice. The Linux verifier is notoriously strict (historically banning many loop shapes and complex pointer arithmetic), produces confusing rejections, and has had CVEs of its own. As Huang and Paradies put it, safety for eBPF as a general-purpose offload mechanism can only be judged relative to the verifier in use: kernel-oriented verifiers reject legitimate data-processing code, and standalone eBPF runtimes like uBPF and rBPF ship much simpler verifiers whose safety “cannot be guaranteed” — rBPF’s own README warns that its JIT does not emit runtime memory checks.

For security rather than mere safety, the linear-memory model also gives Wasm an easier starting point: there is a defined boundary to attack, whereas the verifier is a large, evolving piece of kernel code whose correctness must be maintained against every new instruction and helper.

Performance: Numbers from the DLR Study

The DLR evaluation benchmarked simple arithmetic programs (Fibonacci, integer summing, prime counting) compiled at -Oz across wasmtime 0.29, wasmer 2.0, GraalVM 21.2, rBPF, and uBPF, relative to native binaries (= 1.0), on both x86-64 (Threadripper, kernel 5.13) and Graviton2 arm64 (kernel 5.11).

  • Memory footprint: rBPF and uBPF used ~1.05–1.11× native RSS — near-zero overhead because there is almost no runtime. Wasmtime sat at 3.5–3.9×, wasmer at 12–17× (LLVM ahead-of-time compilation), GraalVM native images at 10–48×.
  • Startup latency: rBPF/uBPF started in 1.6–6.7× native time. Wasmtime took 78–94×, wasmer up to 283×, because of JIT/AOT compilation and runtime initialization.
  • Execution speed: mixed. On the x86 workstation, rBPF/uBPF landed within roughly 0.86–1.11× of native; wasmtime was 2–4× slower on compute-heavy loops; GraalVM was 12–36× slower. On Graviton2, wasmtime’s per-call overhead on tiny functions was much worse (27–51×), while its total time (including startup amortized) was a reasonable 1.3–1.9×.

The pattern is consistent: eBPF VMs are tiny and fast to start; Wasm runtimes are feature-rich and heavier. But the comparison is also honest about its limits — eBPF could not even run the benchmark’s libraries, because there is no libc equivalent. The authors had to manually patch compiled eBPF binaries to simulate function calls and static memory. That is a real ergonomics penalty, not a benchmark artifact.

Portability and Toolchains

eBPF portability is bounded by the kernel: binaries are host-endian (an issue for byte-level parsing of data), the verifier differs across kernel versions (mitigated substantially by CO-RE/BTF), and there is no non-Linux story beyond experimental ports. Tooling is competent but kernel-centric — bpftool can only inspect programs that are already loaded; llvm-objdump is the offline fallback, and eBPF still lacks an official textual assembly format.

WebAssembly is the more portable artifact by design: a wasm32 module is little-endian, runs on any host architecture, and executes on at least three mature, feature-rich runtimes (wasmtime with fuel, caching, async host functions and lldb/gdb debugging; wasmer with tunables and AOT caching; GraalVM with polyglot interop). wabt provides wasm2wat and wasm-objdump for inspection, and wasm-opt from binaryen trims size. Compatibility is also better: wasi-sdk lets you build real libraries (the DLR team compiled zstd for Wasm; the same build fails for eBPF because libc headers are unavailable), and language support extends well beyond C — with the usual caveat that languages must support exporting malloc and cooperating with the host.

Where Each One Wins

Choose eBPF when the kernel is the environment. XDP packet processing, tc-attached policy enforcement, syscall tracing, LSM-based security, in-kernel aggregation of metrics — anything that must see or modify data before it reaches user space, at nanosecond latency, cannot pay a runtime boundary. The data plane is eBPF’s home territory, and no Wasm runtime can replace it: Wasm cannot attach to a NIC’s ingress path.

Choose WebAssembly when the sandbox is the environment. Plugin systems (Envoy Proxy-Wasm, database UDFs, edge workers), portable user-space agents, untrusted third-party code, and cross-OS deployment (Windows included) all favor Wasm: spec-level safety, language freedom, real libc via WASI, and a toolchain designed for people who are not kernel engineers.

Convergence: The Two Stacks Are Joining Up

The interesting story of the last few years is that the dichotomy is dissolving — in both directions:

  • Wasm hosting eBPF: wasm-bpf packages eBPF programs and their user-space control planes as Wasm modules, using WASI as the loader interface, so eBPF agents deploy through ordinary OCI/container toolchains. A compiled eBPF-Wasm module is ~90 KB and loads in under 100 ms.
  • eBPF reaching user space: projects like bpftime run eBPF programs as user-space uprobes/USDT probes, closing eBPF’s gap against Wasm’s tooling for non-kernel workloads.
  • WALI (WASM Application Binary Interface for Linux) standardizes how Wasm modules make syscalls through WASI, the exact interface eBPF helpers formalize on the kernel side.

The DLR paper’s framing holds up: these are complementary offload mechanisms with different centers of gravity. eBPF is how you move computation into the operating system; WebAssembly is how you move computation across operating systems. Picking the wrong one is not merely a performance mistake — it is a category error.

References