Skip to content

The fabric

clientbrowser / SDKterminal · editorhttps · websocketone sessionsupervisor — the session DOsqlite disk · shell · process tableport registry · session agentone-shotnode · py · wasmspawn → exitresidentvite · flask · servekeyed · has portsgit enginefetch · indexchunked checkoutloopback 127.0.0.1:<port> — routed by the supervisorevery facet: its own isolate, its own 128 MiB, evictableplatformpeer DOsinstall shards · heavy facetsR2 + edge cachetarballs · runtimes · bundleshot reads cached per-colo

Select any node — each one says what it owns and how it dies.

The supervisor holds the state that must survive a crash. Everything else runs in facets and can be evicted.

A session is one supervisor Durable Object and a fabric of facets. The supervisor owns the disk, shell, process table, and port registry. Facets do the work. Each process runs in its own isolate with a 128 MiB heap, and any facet can be evicted.

An app can span several isolates. That’s how opencode runs as a server and TUI joined by loopback, with a separate budget for each half.

Workers are stateless and can run anywhere. A sandbox needs one place where the working directory, the process table, and the disk agree. A Durable Object is the only Cloudflare primitive that is both durable and strongly consistent: a single-threaded actor with transactional SQLite storage attached. One session is one DO instance, addressed by tenant:sessionId. There are no replicas, so there is no cache-coherence protocol to run.

The supervisor owns, in one actor:

  • The disk: the SQLite-backed VFS, 10 GB, the single source of truth for every byte (inside the disk).
  • The process table: pid-keyed state that survives facet resets. ps reads the DO, not a facet. Each process carries an immutable credential (uid, gid, groups, umask) assigned at spawn.
  • The port registry: which pid owns which port, and the facet stub that serves it (ports & routing).
  • Per-process log ring buffers and input queues: durable and bounded, so logs <pid> works after a facet dies.
  • Hibernatable WebSockets: terminals survive DO eviction, and configured auto-responses answer idle pings without waking the actor.

The supervisor holds a self-imposed heap ceiling of 64 MiB, half the platform cap. A supervisor OOM resets the whole session instead of failing one process. Nimbus therefore pushes heavy work out to facets.

Facets are dynamically loaded Workers created through the platform’s Worker Loader binding. Each gets the standard nodejs_compat runtime, a 128 MiB heap of its own, and one injected capability. That capability is a SUPERVISOR RPC stub pointing back at its own session. Generated facet code cannot import supervisor modules, so the RPC surface is the whole contract.

The loader keys isolates by content hash and slot, so repeated runs of the same program reuse a warm isolate instead of re-instantiating. A route stub for an evicted facet fails loudly rather than re-loading the code, because a re-loaded isolate is an empty worker with nothing listening.

The supervisor describes a big app with a small spec: argv, env, and a bounded VFS snapshot. The loader assembles the full module map lazily, inside its own cache-miss callback, so the map exists only while a process starts. opencode’s map is roughly 20 MB and a Ruby interpreter image is 34 MiB. Both are read at load time and dropped, not held.

Writes flow one way, and facets never own files. Every mutation streams to the supervisor through a credit pool, so a parallel npm install cannot flood the DO (inside the disk has the numbers).

Every resident process runs as a DO Facet of its own session. A facet is a named child actor whose class comes from a dynamic worker the session loads itself. A node server, a Python or Ruby socket server, and opencode’s TUI and headless server all run this way.

A facet gets its own memory envelope, measured at ~208 MiB each. The figure is the same whether the session holds 0 or 128 MiB, which is what proves the envelope is independent. Eight facets at 192 MiB plus a 128 MiB parent were live at once, 1,664 MiB under one object.

Containment runs both ways. A facet that runs out of memory takes down its own process and leaves the session running. A session that resets leaves the facet’s memory intact.

The SUPERVISOR binding a facet receives carries a doId in its props, and the RPC layer resolves that doId to a stub on every call. So readFile, writeFile, stdout and stderr frames, the stdin pump, and registerPort all land on the session. Compute and resident memory move. The disk, the terminal, and the ports stay where they are.

Because a facet is addressed by name rather than by a transferred entrypoint, the session re-resolves it in any later request context. A resident process can therefore serve a port. Inbound HTTP, SSE, and WebSockets stream straight through the session to the process. A warm facet starts in 8–16 ms.

Facets are separate isolates inside the session’s single actor thread, and that has a cost. A facet awaiting I/O yields the thread. While one parks on a socket, on stdin, or on an outbound call, a sibling runs at idle latency. A facet spending sustained CPU stalls every sibling and its syscalls for that whole duration. So this fabric hosts I/O-bound resident processes. CPU-heavy work such as clang, esbuild, and npm install runs one-shot or fans out across sibling DOs instead.

Constraint Cloudflare’s figure What it produced
Isolate memory 128 MiB each multi-isolate processes; lazily-assembled module maps; one facet per resident process
DO SQLite storage 10 GB per object the session disk
CPU per invocation 30 s default, 5 min via limits.cpu_ms phased git clone, sliced checkout, fan-out installs

A raised cpu_ms applies to the supervisor. Facets keep ~30 s, so long facet work must be split into phases. When the DO hibernates, its disk stays.

Nimbus inherits workerd’s isolation, the same primitives Cloudflare runs untrusted customer code on. The CSP allows no runtime eval and no request-time WASM compilation. Sessions share only read-only, content-addressed caches, and loopback stays inside a session. Security covers the whole model: isolation, Unix permissions, tokens, and the current gaps.