Run a thousand threads.
Pay for one heap.
A worker in Cruft is a Compartment on another thread: its own realm and its own heap, but not a duplicated virtual machine. So you fan work across thousands of workers where worker_threads would run out of memory.
// fan a batch of jobs across a pool of threads const pool = Array.from({ length: 8 }, () => new Compartment({ worker: true, onMessageSource: `(e) => e.data.reduce((a, b) => a + b*b, 0)`, })); const jobs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; const sums = await Promise.all( jobs.map((j, i) => pool[i % pool.length].request(j))); // each request ran on its own thread and heap, in parallel [14, 77, 194]
The isolate tax
A worker without a whole VM
Node's worker_threads gives real threads, but every Worker is a separate V8 isolate: each one duplicates an entire engine heap and its machinery. Workers stay few and chunky because each is expensive. Cruft keeps the isolation at the realm boundary instead of the whole-VM boundary, so a worker is cheap. Spawning one is not a memory event.
// Node: each Worker is a full V8 isolate new Worker("./task.js"); // ~megabytes of VM, each
// Cruft: each worker is a realm on a thread, // over its own heap, not a duplicated engine new Compartment({ worker: true, onMessageSource });
The win, in numbers
One process, over 100,000 workers
Ordinary heaps stay local while shared data does not duplicate per worker, so memory scales sub-linearly in the worker count. Here is one cruft process running a webhook-transform load, from a single worker to 131,072, each delivering 2000 events:
| Workers (one process) | Peak memory | Wall time |
|---|---|---|
| 1 | 61 MiB | 0.3 s |
| 2,048 | 260 MiB | 0.5 s |
| 8,192 | 750 MiB | 2.5 s |
| 32,768 | 2.32 GiB | 7.9 s |
| 65,536 | 3.15 GiB | 18.5 s |
| 131,072 | 3.89 GiB | 65.5 s |
131,072 isolated worker compartments in a single process, roughly 0.03 MiB of marginal cost each, and the curve is still climbing: the ceiling is machine RAM, not anything in the engine. Node's worker_threads spends a whole V8 isolate per worker, so in a paired run at 2,048 workers it used 6,247 MiB to cruft's 260 MiB, and it stalls in the low thousands where cruft keeps climbing.
// ordinary data: copied, a fresh value in the worker c.request({ rows: ["a", "b"] }); // shared bytes: the SAME memory, by reference const sab = new SharedArrayBuffer(8); c.send(sab); // synchronize with Atomics // a function: rejected at send c.send(() => {}); // authority does not cross
Data crosses, authority does not
Three lanes, no data races
A value moving between threads takes one of three lanes, chosen by what it is. Ordinary objects are copied, so shared state is data-race-free by construction. A SharedArrayBuffer is the one mutable lane, explicit and narrow: raw bytes, synchronized with Atomics. A function is refused at the boundary, because authority crosses only by grant, never by smuggling.
Two ways to write one
Native, or the Node idiom
The native API is new Compartment({ worker: true }). Node's worker_threads is an adapter over the same model, so existing code ports and new code can reach for the native surface directly.
Native compartment
const c = new Compartment({ worker: true, onMessageSource: `(e) => e.data * 2`, }); await c.request(21); // 42
node:worker_threads
import { Worker } from "node:worker_threads"; const w = new Worker("./task.ts"); w.postMessage(job);
Parallelism keeps the boundary
A worker is still a compartment
A worker inherits the process capability mode. Under --sealed or --sealed-deps a worker's I/O is gated exactly as the main thread's is, and the worker realm starts with the same empty globals as any compartment. Fanning work across threads does not open a side door around the isolation model.
# the worker's I/O is gated like the main thread's $ cruft --sealed-deps app.mjs // inside the worker realm, ambient authority is absent onMessageSource: `(e) => typeof fetch` // "undefined"
How it works
Workers are realms over per-thread heaps, sharing through three explicit lanes, collected independently. Read the guide, the full API, and the memory model that makes it cheap.