Summary
A persistent stub created via ctx.restore(params) in a Durable Object, stored in DO
storage, then returned out to a different Worker and invoked there, does not make progress:
the invocation sits at 0 CPU until the runtime cancels it. The target's [restore](params)
method is never invoked. The same code path completes normally when all Workers run in a
single process (local dev / one workerd instance).
Environment
wrangler ^4.103.0
compatibility_date: 2026-02-02
compatibility_flags: ["experimental", "allow_irrevocable_stub_storage"]
- Deployment topology: multiple separate, service-bound Workers, plus Durable Objects hosted
in one of them. The behavior was observed only in a real multi-Worker deployment (separate
processes/isolates), not in a single-process runtime.
Setup under which it was observed
Three execution contexts are involved:
- Caller Worker — a separate Worker holding a service-binding
Fetcher to a
WorkerEntrypoint in another Worker.
- Host Worker — hosts both that
WorkerEntrypoint and the Durable Object namespace.
- The DO — a Durable Object in the Host Worker that owns the persistent stub. The stub is
created inside the DO via ctx.restore(params), implements [restore](params), and is stored
in the DO's storage under allow_irrevocable_stub_storage.
The persistent stub is delivered to the Caller Worker by value across two hops, then invoked there:
DO ──(returns persistent stub)──▶ WorkerEntrypoint (Host) ──(returns it)──▶ Caller Worker
Caller Worker: stub.someMethod(args) // <-- hangs
Expected behavior
Invoking a method on the stub from the Caller Worker routes back to the Host Worker / DO, drives
the stub's [restore](params) to rebuild the live target, invokes the method, and returns the
result — as it does when everything runs in one process.
Observed behavior
- The Caller Worker's method invocation hangs. Measured wall-clock durations of ~100–190s at
0 CPU before the runtime cancels the request (in our case via a Durable Object alarm's
wall-clock limit; the alarm is then killed and its buffered logs discarded).
- The DO's
[restore] and the target method never run: no logs from inside [restore] or the
target ever appear, and the DO invocation is ultimately canceled.
- The Caller Worker receives a sanitized
"internal error; reference = <id>" after cancellation.
Additional observations
- Single-process works. With all Workers running in one process (local dev), the same code —
the stub is still passed to the other Worker and invoked there — completes normally.
- Not a pipelining / un-awaited-promise artifact. We changed the intermediate
WorkerEntrypoint hop to await the DO RPC, so the Caller Worker received a resolved, concrete
stub rather than a pipelined promise. The invocation still hung.
- Ordinary cross-Worker RPC is unaffected. Calls on the service-binding
Fetcher, and calls
on ordinary (non-persistent, live) RpcTarget objects returned across the same boundary,
complete normally in the same deployment. Only invoking a [restore]()-backed stub that was
re-exported to another Worker exhibits the hang.
Summary
A persistent stub created via
ctx.restore(params)in a Durable Object, stored in DOstorage, then returned out to a different Worker and invoked there, does not make progress:
the invocation sits at 0 CPU until the runtime cancels it. The target's
[restore](params)method is never invoked. The same code path completes normally when all Workers run in a
single process (local dev / one
workerdinstance).Environment
wrangler^4.103.0compatibility_date:2026-02-02compatibility_flags:["experimental", "allow_irrevocable_stub_storage"]in one of them. The behavior was observed only in a real multi-Worker deployment (separate
processes/isolates), not in a single-process runtime.
Setup under which it was observed
Three execution contexts are involved:
Fetcherto aWorkerEntrypointin another Worker.WorkerEntrypointand the Durable Object namespace.created inside the DO via
ctx.restore(params), implements[restore](params), and is storedin the DO's storage under
allow_irrevocable_stub_storage.The persistent stub is delivered to the Caller Worker by value across two hops, then invoked there:
Expected behavior
Invoking a method on the stub from the Caller Worker routes back to the Host Worker / DO, drives
the stub's
[restore](params)to rebuild the live target, invokes the method, and returns theresult — as it does when everything runs in one process.
Observed behavior
0 CPU before the runtime cancels the request (in our case via a Durable Object alarm's
wall-clock limit; the alarm is then killed and its buffered logs discarded).
[restore]and the target method never run: no logs from inside[restore]or thetarget ever appear, and the DO invocation is ultimately canceled.
"internal error; reference = <id>"after cancellation.Additional observations
the stub is still passed to the other Worker and invoked there — completes normally.
WorkerEntrypointhop toawaitthe DO RPC, so the Caller Worker received a resolved, concretestub rather than a pipelined promise. The invocation still hung.
Fetcher, and callson ordinary (non-persistent, live)
RpcTargetobjects returned across the same boundary,complete normally in the same deployment. Only invoking a
[restore]()-backed stub that wasre-exported to another Worker exhibits the hang.