The process fabric
The process table lives in the supervisor DO, never in facets. A facet’s
module state can vanish when workerd resets its isolate. ps still reads the
DO. Logs and input queues are durable and bounded, and terminals use
hibernatable WebSockets.
Every process also carries an immutable credential — uid, gid, supplementary groups, umask — assigned at spawn and inherited from its parent. The credential travels with every filesystem call the process makes, which is what makes Unix permissions enforceable at one seam.
A keyed resident keeps vite or opencode serve warm across requests. Its
route stub resolves the worker from the loader cache and never boots one,
because a re-loaded isolate is an empty worker with nothing listening. If the
facet is gone, the stub fails loudly. An OOM in a facet returns a catchable
RPC error, so the session survives.
The supervisor stages a big app with a small spec: argv, env, and a VFS snapshot. The Worker-Loader cache-miss path assembles the full module map outside the DO. opencode’s map is ~20 MB. Assembling it inside the DO crossed the memory cap and reset the session, so it runs outside. workerd also blocks request-time WASM compilation, so WASM enters the module map pre-compiled.
Before anything attaches, server facets pass health gates with bounded polls
and an overall budget. The opencode pair starts the server, checks it over
loopback, then attaches the TUI at 127.0.0.1:4096. Their lifecycles are tied,
so exiting one reaps the other. The serve half is stable. The TUI attach half
runs on a thin memory margin and is still
being hardened.
Pipes and process boundaries
Section titled “Pipes and process boundaries”There are three kinds of process boundary, each with a different pipe.
Shell pipelines wire commands with in-memory pipe channels inside one
isolate; sort file | grep -v x never crosses an RPC. A spawned child
(child_process, npm bins) gets its own facet, with stdio flowing over
supervisor RPC and its pid registered in the DO before the facet boots.
Inside the bash runner, WASM instances are connected by real byte-buffer
pipes with blocking reads, so echo a | tr a b produces b through an
actual pipe(2).
fork(), setjmp, and real bash
Section titled “fork(), setjmp, and real bash”V8 isolates have no fork(2), so Nimbus implements one. Asyncify — the
instrumentation that lets WASM unwind and rewind its call stack —
snapshots a running program: the parent’s linear memory is copied, every
exported global including the stack pointer is restored into a sibling
instance of the same module, and both sides rewind. The parent resumes
holding the child pid; the child resumes holding 0. The same machinery
provides asyncify-native setjmp/longjmp, which bash requires.
GNU bash 5.2.37, compiled to wasm32-wasi and asyncified, runs in a
dedicated facet with a scheduler that services fork, exec, waitpid,
blocking pipe reads, and setjmp across multiple live WASM instances in one
isolate. Its coreutils are BusyBox 1.37.0, one multicall binary mapped to
every applet name, exec’d as plain WASI programs and pumped by the same
scheduler. Copying a 16 MiB image on the reuse path costs ~0.5 ms;
fork+exec+wait of a different image is ~1.9 ms. The default interactive
shell is still the Nimbus interpreter;
bash is installed as a program, and folding the fork machinery into
the default process model is tracked on the research
page.