Loam is pre-alpha: the engine core runs today; Live and Durable are in progress. See the roadmap

Blog/What Loam is built on

workerd, wasmtime and gVisor: three ways to run code you did not write

The three isolation technologies behind Loam's proposed function tiers. How V8 isolates, WebAssembly and a user-space kernel each contain code, what each costs, how each can be metered, and why workerd never runs two tenants in one process.

workerd, wasmtime and gVisor: three ways to run code you did not write
On this page
  1. workerd: V8 isolates with the web platform
  2. wasmtime: WebAssembly components
  3. gVisor: a kernel in user space
  4. Why three, and not one
  5. Limits we plan around

Planned These runtimes are part of the function runtime proposal (platform part 5). Loam does not run user code on any of them yet.

A platform that runs other people's functions has to answer two questions for every request: how is this code kept away from everyone else's, and how much CPU did it use? Loam's proposal answers them three ways, one per tier, because no single technology is cheap, compatible and strongly isolated at once.

workerdwasmtimegVisor
What it isCloudflare Workers' JavaScript and Wasm runtimeThe Bytecode Alliance's WebAssembly runtimeGoogle's user-space application kernel
LicenseApache-2.0Apache-2.0 WITH LLVM-exceptionApache-2.0
Version checkedv1.20260929.1 (it releases daily)v49.0.1release-20260921.0
Loam tierT0: JavaScript and TypeScriptT1: Wasm componentsT2: any Linux binary; also the outer wall around T0
Isolation unitV8 isolate (inside a process)Wasm instance (inside a process)Sandbox (its own kernel, per pod)
Start costMilliseconds for an isolateMicroseconds to millisecondsHundreds of milliseconds
CPU meterThe process's cgroupFuel or epochs, per invocationThe sandbox's cgroup

workerd: V8 isolates with the web platform

workerd is the open-source runtime at the heart of Cloudflare Workers. It runs JavaScript (TypeScript once compiled to it) and WebAssembly in V8 isolates: separate heaps inside one process, each with its own globals, created in milliseconds and costing a few megabytes. Around them it implements the web platform APIs (fetch, Request and Response, streams, Web Crypto, URL), a Node.js compatibility layer, and capability-based bindings: a worker can only reach what its configuration gives it, such as a named service, a KV namespace or an R2 bucket. Configuration is a Cap'n Proto file describing services, sockets and bindings.

Why Loam would use it. Framework adapters for Cloudflare already produce workerd-compatible output: Hono, Astro, SvelteKit, Nuxt through Nitro, React Router, and Next.js through OpenNext. Running workerd means those builds run without a Loam-specific port, and Loam does not have to write and maintain the web platform on a bare JavaScript engine. Its KV and R2 bindings become HTTP requests to a named service, which Loam's node supervisor can implement over Loam storage.

The caveat, in its own words. workerd's README says: "workerd on its own does not contain suitable defense-in-depth against the possibility of implementation bugs. When using workerd to run possibly-malicious code, you must run it inside an appropriate secure sandbox, such as a virtual machine." Cloudflare's hosted service adds defenses that are not all in the open-source runtime. So Loam's design never shares a workerd process between tenants: each tenant gets its own process, inside gVisor where the node supports it (or seccomp, a user namespace and a cgroup where it does not), and isolates separate only one tenant's functions and versions from each other.

Metering. workerd's open-source configuration has no per-request CPU limit. CPU is measured per process, from the tenant's cgroup (cpu.stat), cross-checked with getrusage. That is exact per tenant but only an estimate per invocation when one process serves many.

wasmtime: WebAssembly components

wasmtime runs WebAssembly, a portable bytecode with a simple safety model: a module can only touch its own linear memory, bounds-checked (often for free, with guard pages), and can only call the functions its host imports into it. It compiles modules ahead of time or just in time with its Cranelift code generator. It implements the component model and WASI 0.2, so a component can declare that it exports wasi:http/incoming-handler and be served as an HTTP function whatever language it was written in: Rust, Go through TinyGo, Python through componentize-py.

Metering and limits are where wasmtime shines for a CPU-billed platform:

  • Fuel. The compiled code decrements a counter as it executes; when fuel runs out, execution traps. It is exact and deterministic, at some runtime cost.
  • Epoch interruption. A cheaper, coarser alternative: a timer increments an epoch, and code checks it at loop back-edges and function entries.
  • The pooling allocator pre-reserves memory slots for instances, so instantiation per request takes microseconds.

Why wasmtime first. It is the reference implementation of the component model, it is Rust, and fuel gives a deterministic per-invocation count of Wasm work even when many tenants share one host process. Before writing our own host, the plan evaluates Spin and wasmCloud, both Apache-2.0 and both built on wasmtime.

gVisor: a kernel in user space

gVisor runs ordinary Linux programs (Node, Bun, Python, Go binaries, anything) with a much smaller attack surface than a container. Its core, the Sentry, is an application kernel written in Go that implements Linux system calls in user space. The sandboxed program's system calls are intercepted (with the systrap platform, using seccomp, or with KVM where available) and served by the Sentry, which itself uses only a small, filtered set of host system calls. File access goes through a separate proxy. On Kubernetes it runs as an OCI runtime, runsc, selected with a RuntimeClass.

The cost is overhead on system-call-heavy work and slower starts than an isolate: hundreds of milliseconds rather than milliseconds. Some kernel features are limited; io_uring, for example, is restricted, which is why Loam's design keeps io_uring to its own data plane and never hands it to tenant sandboxes.

In Loam's design, gVisor is the T2 tier for apps that listen on a port (Express, Fastify, Django, Rails), and the outer sandbox around each tenant's workerd process. Metering reads the sandbox pod's cgroup.

Why three, and not one

  • Only isolates are cheap enough to keep thousands resident while they wait on models, which is the whole point of CPU billing. But isolates alone are not a security boundary between tenants.
  • Only Wasm gives a deterministic per-invocation work count inside a shared process, and polyglot components. But most web code today is JavaScript written for Node or Workers, not Wasm.
  • Only a sandbox like gVisor runs arbitrary binaries. But it is too heavy to start per request.

Firecracker microVMs restored from snapshots would add a fourth tier with microsecond resumes; that is deferred, because Kata Containers' Firecracker driver does not actually restore snapshots and the alternative needs more orchestration than a first phase should carry.

Limits we plan around

  • workerd releases daily and its compatibility dates move. Loam would pin a release per Loam version and set each function's compatibility date from its manifest.
  • Hosted Workers features are not all in open-source workerd. Which adapter outputs break is a spike item, answered with a published support matrix.
  • Per-tenant workerd processes cost memory at high tenant counts; idle tenants are evicted from the resident pool.
  • Whether Resonate's TypeScript SDK runs inside workerd unchanged is unverified; the fallback is a thin shim over fetch to the Resonate protocol.

The next post covers the authorization service that would decide what every one of these functions, and every user, may touch: OpenFGA.

More from the blog