Blog
4 min read

WebAssembly as a Sandbox: Running Untrusted Code With Wasmtime and WASI

WebAssembly's design makes it a strong in-process sandbox: linear memory, no ambient authority, and capability-based access through WASI. How the isolation works, limiting CPU with fuel and epochs, memory limits, the component model, real uses for plugins and user code, and where it falls short.

WebAssembly (Wasm) was built to run untrusted code from the web safely inside a browser. That same design makes it attractive outside the browser: run customer-provided plugins, user scripts or model-generated code inside your own process, with strong isolation and microsecond start-up.

Why Wasm is sandboxed by construction

Linear memory

A Wasm module sees one (or a few) contiguous byte arrays — its linear memory. Every load and store is bounds-checked against that memory (often for free, using guard pages and virtual memory tricks on 64-bit hosts). A module cannot address host memory — no pointers into your process, no reading your secrets.

Structured control flow

Wasm has no arbitrary jumps. Functions are called through typed tables checked at runtime; the call stack isn't addressable from the module. Classic code-injection techniques (overwriting return addresses) don't apply in the same way.

No ambient authority

A Wasm module can do nothing by itself: no files, no network, no clock, no randomness — not even printing. Everything comes from imports the host chooses to provide. This is capability-based security: the host passes in exactly the functions (capabilities) the module may use. (Principle of least privilege)

WASI: system access, on your terms

WASI (WebAssembly System Interface) standardises those imports — filesystem, clocks, random, sockets, HTTP — as capabilities granted by the host. WASI 0.2 (Preview 2) is built on the component model, with interfaces like wasi:filesystem, wasi:http and wasi:cli.

With Wasmtime, a guest gets only the directories you preopen:

use wasmtime::*;
use wasmtime_wasi::{WasiCtxBuilder, DirPerms, FilePerms};

let wasi = WasiCtxBuilder::new()
    .inherit_stdout()
    .preopened_dir("/srv/jobs/123", "/work", DirPerms::all(), FilePerms::all())?
    // no network, no env vars, no other paths
    .build_p1();

The guest can't open /etc/passwd — the path isn't in its world.

Limiting CPU and memory

Isolation isn't enough for untrusted code; it also mustn't run forever or eat all RAM.

  • Fuel — Wasmtime counts executed instructions and traps when the budget runs out. Deterministic, some overhead.
  • Epoch interruption — a cheap periodic check against a counter the host increments; good for wall-clock-style timeouts.
  • Memory limits — cap linear memory growth (and table sizes) with a resource limiter.
  • Stack limits — prevent deep recursion from exhausting host stack.
let mut config = Config::new();
config.consume_fuel(true);
let mut store = Store::new(&engine, state);
store.set_fuel(10_000_000)?;          // trap after ~10M units of work
store.limiter(|s| &mut s.limits);     // StoreLimits with max memory

Start-up and density

Instantiating a pre-compiled module takes microseconds, with memory measured in kilobytes to megabytes. You can run thousands of isolated instances in one process — far denser than containers or VMs. That's why edge platforms and plugin systems like it.

Where it's used

  • Plugin systems — Envoy/proxy filters, database extensions, editor and design-tool plugins, Shopify Functions.
  • Edge and serverless — Fastly Compute and Fermyon run Wasm; Cloudflare Workers supports Wasm alongside JavaScript isolates. (V8 isolates, What is serverless?)
  • User-defined logic in SaaS products — formulas, transforms, workflow steps.
  • Code execution for AI — running model-generated Python or JavaScript compiled to/interpreted in Wasm (e.g. Python via Pyodide or CPython-on-WASI). (Run AI-generated code safely)

Where it falls short

  • Language and library support — great for Rust, C/C++, Go (TinyGo and standard Go's WASI port), AssemblyScript; dynamic languages run as interpreters compiled to Wasm, with performance and package limitations (native extensions often don't work).
  • Threads, sockets and async — still maturing across runtimes; WASI 0.3 adds native async.
  • The runtime is the trusted computing base — a JIT/compiler bug in the runtime can break isolation. Runtimes like Wasmtime invest heavily in fuzzing and formal methods, but defence in depth (running the host process with OS sandboxing) is wise for hostile code. (Landlock, seccomp)
  • Side channels — Spectre-style attacks across instances in one process need mitigations; for mutually distrusting tenants, process or VM separation is safer.
  • Not a full environment — an agent that needs git, npm install, a database and a shell needs a real OS sandbox, not a Wasm module.

When to choose Wasm

Need Good fit?
Small, untrusted functions with defined inputs/outputs Excellent
Thousands of tenants' plugins in one process Excellent
Running arbitrary existing Linux programs Poor — use containers/VMs
Full dev environment for an AI agent Poor — use a VM (Why AI coding agents need persistent workspaces)

EasySpawn gives AI agents the opposite of a tiny Wasm sandbox: a full Linux VM per server where they can run real toolchains — isolated from every other tenant by the hypervisor. See how it works or join the waitlist.

Related: V8 Isolates · Firecracker vs gVisor vs Containers · Run AI-Generated Code Safely · The Sandbox Is the Wrong Abstraction

Keep reading