Workers

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.

pool.ts
// 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 memoryWall time
161 MiB0.3 s
2,048260 MiB0.5 s
8,192750 MiB2.5 s
32,7682.32 GiB7.9 s
65,5363.15 GiB18.5 s
131,0723.89 GiB65.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 full Workers guide →

# 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.