Compute
Multiple Workers
Service bindings, cross-Worker Durable Objects, and dispatch namespaces in one orange dev session.
Status: Partial
Configuration
Pass one Wrangler configuration per Worker, like wrangler dev -c a.toml -c b.toml:
orange dev -c api/wrangler.jsonc -c auth/wrangler.jsonc
- The first Worker is the primary Worker. Only it serves
--port. - Every Worker must set
name. Bindings address other Workers by name. - All Workers store data in the primary Worker’s
.open-orange/directory.
// api/wrangler.jsonc
{
"name": "api",
"services": [
{ "binding": "AUTH", "service": "auth" },
{ "binding": "ADMIN", "service": "auth", "entrypoint": "Admin" }
],
"durable_objects": {
"bindings": [{ "name": "SESSIONS", "class_name": "Session", "script_name": "auth" }]
},
"dispatch_namespaces": [{ "binding": "DISPATCHER", "namespace": "customers" }]
}
Implementation
- Each Worker runs in its own runtime process. All runtimes use one Rivet
Engine. Each Worker serves its actors from its own envoy pool, so a Durable
Object with
script_nameruns in the Worker that exports its class. - Service
fetch()and JS RPC go over loopback HTTP to the callee runtime. RPC uses the same transport as Durable Object RPC. Errors keep their type, message, and own properties, and are markedremote. env.DISPATCHER.get(name)returns a Fetcher for the session Worker with thatname. Pass user Workers with more-coptions.- A Workflow binding with
script_namecreates and reads instances in the Worker that exports the class. A missing Worker throwsWorker "<name>" not found. Make sure it is running locally. - Queues and Workflows are shared by name across the session, as across a Cloudflare account. A message that any Worker sends goes to the Worker that declares the queue consumer. A Workflow instance runs in the Worker that defines the Workflow.
- D1 databases, KV namespaces, R2 buckets, and rate limiters are shared by ID
across the session, as in a multi-Worker
wrangler dev. The ID isdatabase_id,id, orbucket_name, and the binding name when the config has none. A Worker reads what another Worker wrote. Tested intests/session-shared-storage.test.ts. - Each Worker keeps its own cron triggers. Orange adds the Worker name to the cron actor key. A Worker that runs alone keeps its old key, so its local data stays.
orange devfails at startup when two Workers declare a consumer for one queue, or define a Workflow with the same name.
Several Workers in one deployment
orange serve takes -c more than once, as orange dev does. One image then
runs every Worker, each in its own runtime. All runtimes join the cluster’s one
envoy pool, because a cluster’s Engine accepts only the pools its operator
created. The Engine sends an actor only to an envoy that serves the actor’s
name, and each Worker’s actors have their own names. The Workers reach each
other as in orange dev.
orange serve -c app/wrangler.json -c api/wrangler.json -c jobs/wrangler.json
- The serve port answers with the Worker whose
routesmatch the request’s host and path. The most specific pattern wins: an exact host before a*.host, then the longer path, then an exact path before a*path. Acustom_domainroute matches every path of its host. A request that no route matches goes to the first Worker. - A route may use
*only at the start of its host and the end of its path.orange serverefuses any other pattern at startup and names it. - A cluster has one hostname. To serve another host through it, a forwarder
in front of the cluster sends
x-orange-forwarded-hostandx-orange-forwarded-host-signature, an HMAC-SHA256 of the host with the secret inORANGE_FORWARDED_HOST_SECRET. With a matching signature, the request is routed and seen as that host. Without the secret or with another signature, the headers are ignored. A Worker never sees either header. - One Worker exiting stops the deployment, so the cluster starts the image again.
- Only the first Worker’s runtime serves
RIVETKIT_METRICS_PORT, so the cluster’s metrics cover that Worker’s actors only. - Tested in
tests/orange-serve.test.tsagainst a standalone Engine.
Workers that are not running
Orange follows Wrangler’s dev registry:
fetch()on a service binding or Durable Object stub answers503withWorker "<name>" not found. Make sure it is running locally.- RPC calls throw an
Errorwith the same message. - A dispatch namespace throws
Worker not found. Worker "<name>" is not running in this orange dev session.The message starts like Cloudflare’s.
Conformance
src/runtime/multi-worker.test.tsruns two Workers throughorange devand checks servicefetch(), RPC, error types, shared Durable Object state, and missing Workers. It also checks that each Worker gets its own cron ticks, that a queue that one Worker produces to reaches the consumer in the other, and that two consumers for one queue fail at startup.
Known differences
connect()and WebSocket upgrades through a service binding to another Worker throw an error. Connect to the other Worker’s URL instead.- Workers in separate
orange devprocesses do not find each other. Run them in oneorange dev. - Dispatch namespaces ignore
args,limits, and outbound Workers. Orange does not check that a user Worker belongs to the namespace. tail_consumerscan name another Worker of the session. See Tail Workers.orange d1reaches the session’s databases only with the primary Worker’s configuration, the first-c. With another Worker’s configuration it opens a separate, empty database.
Compatibility
From the latest conformance run. Scores on the compatibility page; every binding on the status page.
Service bindings to other Workers
services Runs on One runtime process per Worker in one orange dev session (orange dev -c a.jsonc -c b.jsonc), with fetch() and RPC over loopback HTTP.
fetch(), JS RPC, and named entrypoints work. connect() to another Worker works; WebSocket upgrades to another Worker throw. Workers in separate orange dev processes do not find each other.
Recorded results
WorksService bindings and dispatch namespacesAvailable.Across the Workers of one orange dev session. orange deploy ships one Worker at a time, so a binding to another Worker has nothing to reach on a pool. 2/2 pass
- Results
- example: multi-worker Pass
- example: rpc-entrypoints Pass
Worksconnect() through a service binding to another WorkerAvailable.A call reaches the connect() handler in another Worker of the orange dev session. 1/1 pass
- Results
- api-tests--connect-handler-test > localAddressViaServiceBinding Pass
Worksqueue() and scheduled() on a service bindingAvailable.Both run the handler and report its outcome. queue() reads workerd's serializedBody format, and a crafted payload fails the event. A serializedBody holding a stream, stub, or abort signal also fails the event. A decoded Headers isn't immutable, because Orange has no Headers guard. Unlike connect(), both calls reject when the binding points at another Worker of an orange dev session. 2/2 pass
- What to do
- In
orange dev, callqueue()andscheduled()on a binding to the same Worker. Send plain values inserializedBody. - Results
- api-tests--deserialize-hardening-test Pass 6/6
- api-tests--queue-metadata-test > queue-metadata-test Pass
In the feature matrix
Supportedservices (this Worker's default entrypoint)A service binding can target the Worker itself.A binding whose service equals the config name calls the default export in process. 1/1 pass · 3 files
- Notes
- The config must set name. Without name, Orange treats every service binding as a binding to another Worker and loads a stub.
- Tests
-
- api-tests--js-rpc-test › basicServiceBinding Pass
- Source
- src/config/load-wrangler.ts:776-786packages/runtime-node/src/runtime.ts:133-144packages/runtime-node/src/bindings/service.ts:50-67
Partialservices with entrypoint (named entrypoint in this Worker)entrypoint selects a named WorkerEntrypoint export of the target Worker.Calls the named export of this Worker. A missing export fails with Worker has no entrypoint named "<name>". 0/2 pass · 2 files
- Notes
- The repository end-to-end test passes. Upstream namedServiceBinding hangs for 60 seconds in all four js-rpc test files.
- Not verified by a test
- The cause of the namedServiceBinding hang is not found.
- Tests
-
- api-tests--js-rpc-test › namedServiceBinding TimeoutError: The operation was aborted due to timeout Fail · Hangs (60s timeout)
- api-tests--js-rpc-socket-test › namedServiceBinding TimeoutError: The operation was aborted due to timeout Fail · Hangs (60s timeout)
- Source
- packages/runtime-node/src/bindings/service.ts:81-157src/runtime/service-binding.test.ts:49
Supportedservices with propsprops on a service binding appear as ctx.props in the target entrypoint.Config props and ctx.exports.<Name>({ props }) reach ctx.props. Overlapping calls see only their own props. 1/1 pass · 4 files
- Notes
- ctx-props-test fails only its ctx-props-test-disabled case, which tests a Worker without the props flag.
- Tests
-
- api-tests--ctx-props-test › ctx-props-test-disabled Pass
- Source
- src/config/load-wrangler.ts:782packages/runtime-node/src/bindings/service.ts:97-124packages/runtime-node/src/bindings/service.ts:477-492packages/runtime-node/src/bindings/service.test.ts:316-354
SupportedService binding fetch()fetch() on a service binding sends an HTTP request to the target's fetch handler.Calls the entrypoint's fetch() in process. It follows redirects like workerd's fetch() and passes caller aborts to the callee. 3 files
- Notes
- In a Durable Object, the request waits until earlier writes are durable.
- Source
- packages/runtime-node/src/bindings/service.ts:377-438packages/runtime-node/src/bindings/service.ts:461-476packages/runtime-node/src/bindings/service.test.ts:84-315
PartialService binding JS RPCMethods and properties of a WorkerEntrypoint are callable over JS RPC.Each call gets a new entrypoint instance in process. js-rpc-test passes 28 of 33. 3/5 pass · 2 files
- Notes
- Fails: disposal (not implemented), namedServiceBinding and portAbortCall (hang), sendStubOverRpc, and serializeHttpTypes. See the runtime API entries for RPC details.
- Tests
-
- api-tests--js-rpc-test › basicServiceBinding Pass
- api-tests--js-rpc-test › disposal Pass
- api-tests--js-rpc-test › portAbortCall Pass
- api-tests--js-rpc-test › sendStubOverRpc AssertionError [ERR_ASSERTION]: Missing expected rejection (Error). Fail · Behavior differs
- api-tests--js-rpc-test › serializeHttpTypes AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: | + actual - expected | Fail · Behavior differs
- Source
- packages/runtime-node/src/bindings/service.ts:159-216packages/runtime-node/src/rpc/loopback.ts
SupportedService binding connect()connect() opens a socket to the target Worker's connect handler.Opens an in-process socket to the entrypoint's connect() handler of this Worker. In a Durable Object, the connection opens after earlier writes are durable. The handler's end closes when its promise settles. 3/3 pass · 3 files
- Notes
- A target without connect() fails with Handler does not export a connect() function. A binding to another Worker is a stub (see Service binding to another Worker). js-rpc-test connectBinding passes. connect-handler-test localAddressViaServiceBinding fails only because the conformance runner drops its binding.
- Tests
-
- api-tests--js-rpc-test › connectBinding Pass
- api-tests--connect-handler-test › localAddressViaServiceBinding Pass
- api-tests--connect-neuter-test › connectNeuterRegression Pass
- Source
- packages/runtime-node/src/bindings/service.ts:330-366packages/runtime-node/src/bindings/service.ts:455-457packages/runtime-node/src/bindings/service.test.ts:355
Partialservices (another Worker)Calls a different Worker over fetch, JS RPC, or connect().orange dev -c a -c b runs both Workers. fetch() and JS RPC reach the other Worker's default or named entrypoint over loopback HTTP. A Worker outside the session answers 503 on fetch() and throws on RPC with Wrangler's dev registry message. 3 files
- Notes
- connect() and WebSocket upgrades to another Worker throw. Workers in separate orange dev processes do not find each other.
- Workaround
- Pass every Worker to one orange dev with repeated -c.
- Source
- packages/runtime-node/src/session/client.tspackages/runtime-node/src/session/server.tssrc/runtime/multi-worker.test.ts
Workers for Platforms (dispatch namespaces)
dispatch_namespaces Runs on env.DISPATCHER.get(name) finds the user Worker by name among the Workers of the orange dev session.
Pass user Workers with more -c options. args, limits, and outbound Workers are ignored, and namespace membership is not checked.
Recorded results
WorksService bindings and dispatch namespacesAvailable.Across the Workers of one orange dev session. orange deploy ships one Worker at a time, so a binding to another Worker has nothing to reach on a pool. 2/2 pass
- Results
- example: multi-worker Pass
- example: rpc-entrypoints Pass
In the feature matrix
Partialdispatch_namespaces (dispatch_namespace)Calls user Workers in a Workers for Platforms dispatch namespace.get(name) returns a Fetcher for the Worker of the orange dev session with that name. A missing Worker throws Worker not found. 3 files
- Notes
- args, limits, and outbound Workers have no effect. Namespace membership is not checked.
- Workaround
- Pass user Workers to orange dev with repeated -c.
- Source
- packages/runtime-node/src/session/client.tspackages/runtime-node/src/session/server.tssrc/runtime/multi-worker.test.ts