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, and 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. A stub
whose facet is gone 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.
Both halves run the way every resident process does, as a facet of the session with a memory envelope of its own. The serve half binds a port and answers over it. The attach half sends frames over the terminal RPC and takes keystrokes over the stdin pump. The serve half is stable. The attach half 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. Stdio flows over
supervisor RPC, and the pid is registered in the DO before the facet boots.
Inside the bash runner, the scheduler connects WASM instances with real
byte-buffer pipes and blocking reads. echo a | tr a b produces b
through a 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, and 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. Its scheduler 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, and
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.