Compute
Worker Loader
Load and run dynamic Workers at runtime with the worker_loaders binding in OpenOrange.
Status: Partial
Configuration
{
"worker_loaders": [{ "binding": "LOADER" }]
}
How OpenOrange implements it
- Dynamic Worker code is untrusted, so each dynamic Worker runs in its own Node.js process inside an agentOS VM, never on the host. It does not share globals, module state,
process.env, orenvwith the parent Worker. If the VM cannot boot,get()andload()calls fail with the boot error. OpenOrange never falls back to the host. - The parent and the dynamic Worker talk Workers RPC over Cap’n Web, framed on the dynamic Worker process’s stdin and stdout. The dynamic Worker’s own stdout output still reaches the parent’s stdout. Stubs,
RpcTargets, functions, service bindings, and Durable Object stubs inenv,props, arguments, and results travel by reference. get(name, getCode)keeps one Worker per name in each binding.get(null, ...)andload(code)always start a new Worker.- Supported module types: string
.js(ES module),{ js },{ cjs },{ text },{ data },{ json }, and{ wasm }with bytes. Relative imports,cloudflare:*, andnode:*imports resolve like the parent Worker. globalOutbound: nullmakesfetch()reject andconnect()throw with the workerd message. A Fetcher receives the Worker’sfetch()calls.- If a dynamic Worker process exits, its pending calls reject and the next
get()with that name loads it again. Stoppingorange devstops every dynamic Worker process. - Each dynamic Worker gets its own VM, as each gets its own isolate on Cloudflare. Two dynamic Workers do not share a filesystem, processes, or loopback. A Worker with the default
globalOutboundgets a VM with network access. A Worker withglobalOutbound: nullor a Fetcher outbound gets a VM with no network. The VM is disposed when the dynamic Worker process exits ororange devstops.pnpm buildemits the VM bundle todist/runtime/worker-loader-vm/. - Each VM boots on demand when its dynamic Worker loads, so the first call to a dynamic Worker waits for the boot of about 1 s. If the boot fails, that call throws the error.
- The agentOS VM runs Node.js 22. The VM bundle fills missing Web globals from
node:stream/web,node:buffer,node:crypto, andnode:perf_hooks, buildsCompressionStreamandDecompressionStreamonnode:zlib, addsURLPatternfromurlpattern-polyfill, and setsnavigator. Any other global that the VM lacks becomes a class that throws when constructed. Workerd stream fixes that read Node.js 24 stream internals do not apply in the VM, and dynamic Workers there use the VM’s ownWebSocketclass.
Conformance coverage
src/runtime/worker-loader.test.ts drives a real orange dev over HTTP: fetch and RPC, env and props capabilities, isolation, globalOutbound: null, name caching, syntax errors, process crashes, and module types.
Known differences from Cloudflare
compatibilityDate,compatibilityFlags,allowExperimental, andlimitsare accepted and ignored. The dynamic Worker uses the same compatibility profile as the parent.- OpenOrange does not copy Cloudflare’s 64 MiB code and 1 MiB
envlimits. The limits are agentOS’s, which runs the dynamic Worker: its VM filesystem holds 64 MiB by default, including OpenOrange’s own runtime files, and one file is written as one message of at most 16 MiB. A larger module fails with aRangeErrorthat names that limit. In a test with agentOS 0.4.1, a 5 MiBenvworked and a 10 MiBenvmade the dynamic Worker exit with code 1. tailsandstreamingTailsare accepted and receive no events.getDurableObjectClass()throws aNotSupportedError.connect()on a dynamic entrypoint, andconnect()through a FetcherglobalOutbound, throw aNotSupportedError-style error.- Python modules and compiled
WebAssembly.Modulevalues throw aTypeError. Pass Wasm bytes as{ wasm: ArrayBuffer }. globalOutbound: nullblocks the globalfetch()andcloudflare:socketsconnect(). The VM with no network also blocksnode:netandnode:httpconnections.node:fssees only the VM filesystem, not host files, and a dynamic Worker cannot connect to host loopback ports, whatever itsglobalOutbound.- A Worker that fails to load is forgotten, so the next
get()with that name callsgetCodeagain. Workerd keeps the failed Worker. - Orange allows dynamic entrypoint stubs and uncalled
ctx.exportsstubs inprops, where workerd throwsDataCloneError. - Dynamic Worker processes are not unloaded while idle. Each
load()keeps a process untilorange devstops.
Compatibility
From the latest conformance run. Scores on the compatibility page; every binding on the status page.
Worker Loader
Partial
worker_loaders Runs on One Node.js process per dynamic Worker in an agentOS VM, never on the host, Workers RPC over Cap'n Web.
get, load, fetch, and RPC work. getDurableObjectClass(), Python modules, and compiled Wasm modules throw.
In the feature matrix
Partialworker_loaders (worker_loader)Loads and runs dynamic Workers from code at runtime.Each dynamic Worker runs in its own Node.js process. get() caches by name, load() and get(null) start fresh Workers. fetch(), RPC, env and props capabilities, globalOutbound null or Fetcher, and js/cjs/text/data/json/wasm modules work. 2 files
- Notes
- getDurableObjectClass(), entrypoint connect(), Python modules, and compiled WebAssembly.Module values throw. Compatibility settings, limits, and tails are ignored. See docs/modules/worker-loader.mdx.
- Source
- packages/runtime-node/src/bindings/worker-loader/loader.tssrc/runtime/worker-loader.test.ts