Workers Available

Run ES modules on separate isolates with narrowed permissions, structured-clone messaging and per-class resource quotas.

Available

A Velyr worker is a separate isolate on its own OS thread, with its own JS heap, microtask queue, event loop, permission context and quota context (ADR-010, ADR-014, docs/specs/worker-api.md). Nothing mutable is shared between isolates: workers talk to the parent by message passing.

Quick start#

main.mjs
const worker = new Worker(new URL("./square.mjs", import.meta.url), { type: "module" });

worker.onmessage = (event) => {
  console.log(event.data); // 4, 9, 16
};
for (const n of [2, 3, 4]) worker.postMessage(n);
square.mjs
self.onmessage = (event) => {
  self.postMessage(event.data * event.data);
};
bash
velyr run main.mjs

A worker that is still alive keeps the parent running. Call worker.terminate() when you are done, or Velyr.unrefWorker(worker) to let the parent exit while the worker runs.

JS API#

APIBehaviorStatus
new Worker(url, options)Spawns a module worker. url may be a string or URL; a relative URL resolves against the module that called new WorkerAvailable
options.typeOnly "module" (the default). Any other value, including "classic", throws TypeErrorAvailable
options.nameSets the worker's nameAvailable
options.velyr.permissionsNarrows the worker's authority; see below. A Velyr extension, kept out of standard optionsAvailable
worker.postMessage(message, transfer)Structured-clones message to the workerAvailable
worker.onmessage, onmessageerror, onerror and addEventListenerStandard event handlers; error events carry message, filename, lineno, colnoAvailable
worker.terminate()Stops the worker (order below)Available
Velyr.unrefWorker(worker)Lets the parent exit while the worker is still runningAvailable
MessageChannel, MessagePort, BroadcastChannelA transferred port talks directly to the worker; BroadcastChannel reaches across isolatesAvailable

Inside a worker the global object also provides self, name, postMessage, close, reportError, and addEventListener/removeEventListener/dispatchEvent. Workers can spawn workers.

Permissions#

A worker never gains authority its parent lacks. The effective set is:

effective = parent ∩ requested ∩ deployment_policy
  • With no velyr.permissions, the worker may use whatever its parent can.
  • With velyr.permissions, the object uses the same grammar as the CLI flags and the permissions block of velyr.json (kebab-case keys such as "allow-env"), and can only narrow what the parent holds.
  • The velyr run CLI has no separate deployment policy source, so the policy is the main isolate's own authority.
narrow.mjs
const worker = new Worker(new URL("./job.mjs", import.meta.url), {
  type: "module",
  velyr: { permissions: { "allow-env": ["HOME"] } },
});

Run as velyr run --allow-env=HOME,USER narrow.mjs, the worker sees HOME but not USER. Run with only --allow-env=HOME, a worker that asks for "allow-all": true still gets only HOME. Both cases are covered by crates/cli/tests/workers.rs.

Messages and structured clone#

Messages are serialized with the engine's structured clone (V8's ValueSerializer in engine-v8) into a SerializedValue, moved over a channel, and deserialized in the receiving isolate. Ports can be transferred.

Each channel has a bounded queue. When it is full, postMessage fails with VELYR_QUOTA_EXCEEDED instead of letting memory grow (limit 10000 in the test fixture).

Resource quotas#

Quotas are per-class counters that fail an operation with QuotaExceeded rather than exhausting the host. They come from the runtime-resources crate and are set with the quotas key of velyr.json; unset classes use the defaults.

QuotaClassvelyr.json keyDefault limit
Filesfiles4096
Socketssockets16384
Listenerslisteners1024
Workersworkers64
Timerstimers1,000,000
Body bytes in flightbody_bytes1 GiB (1073741824)
velyr.json
{
  "quotas": { "workers": 4, "timers": 10000 }
}

Each worker reserves one workers unit from its parent's quota for as long as its thread runs. A worker's heap limit never exceeds the parent's, and nested workers inherit it. For the config schema see Configuration.

Termination#

terminate() follows the spec order: new worker ops start failing and the shutdown token is cancelled; running JS is interrupted; the event loop stops and discards late completions; the op context closes every resource the worker held; the isolate is destroyed. A worker stuck in a busy loop is stopped this way (terminate_stops_a_busy_worker in the CLI tests).

Not implemented#

  • Classic workers (type: "classic", importScripts).
  • Explicit shared memory. The default is no shared mutable JS state; docs/architecture/scheduler-workers.md treats shared memory as a separate future feature with its own semantics.
  • Internal server workers for multicore serving. The public Worker API and the planned server scheduler are separate layers by design.