Skip to content

🐛 Bug: cross-Worker invocation of a [restore]()-backed persistent stub hangs at 0 CPU #6854

Description

@ndisidore

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:

  1. Caller Worker — a separate Worker holding a service-binding Fetcher to a
    WorkerEntrypoint in another Worker.
  2. Host Worker — hosts both that WorkerEntrypoint and the Durable Object namespace.
  3. 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions