Summary
In a single Durable Object method, the second back-to-back call to a
WorkerEntrypoint RPC method over a service binding (env.MY_BINDING.someMethod(...))
returns a Promise that never settles β it neither resolves nor rejects.
The first call returns normally (~200ms). The second hangs until the
caller-side timeout fires. The hang is independent of:
- the RPC method name,
- the argument shape,
- the response size,
- whether the first call completed before the second was issued.
Switching the transport on the same service binding to fetch
(env.MY_BINDING.fetch(new Request(...))) makes the bug disappear β both
back-to-back calls return cleanly. So this is specific to the RPC
dispatch path, not the service binding itself.
Environment
workerd 1.20260504.1 (2026-04-29)
compatibility_flags: ["nodejs_compat"]
- Paid Workers plan, single Cloudflare account, single Worker fronting the binding
- Caller is a Durable Object method (no Workflows, no special harness)
Reproduction
Caller worker has a service binding pointed at a target WorkerEntrypoint
named TargetEntrypoint. Inside a DO method on the caller side, make two
back-to-back RPC calls:
// Inside a DO method on the caller worker
async probeRpcs(ctx: { entraOid: string }) {
const binding = this.env.MY_BINDING; // service binding to TargetEntrypoint
// Call #0 β returns normally
const a = await binding.callTool(
{ entraOid: ctx.entraOid },
{ name: "get_recent_meetings", arguments: {} }
);
// Call #1 β Promise never settles (no resolve, no reject)
const b = await binding.callTool(
{ entraOid: ctx.entraOid },
{ name: "get_recent_meetings", arguments: {} }
);
return { a, b };
}
The TargetEntrypoint side is a stock WorkerEntrypoint subclass:
import { WorkerEntrypoint } from "cloudflare:workers";
export class TargetEntrypoint extends WorkerEntrypoint<Env> {
async callTool(context: { entraOid: string }, request: { name: string; arguments?: unknown }) {
// ... real work; returns { content: [{ type: "text", text: "..." }] } in ~200ms
}
}
Transport matrix (same service binding, same target, two back-to-back calls)
| Transport |
Call #0 |
Call #1 |
binding.callTool({entraOid}, {name, arguments}) (RPC entrypoint) |
β
~193ms |
β 20000ms caller timeout, Promise never settles |
binding.fetch(new Request("https://target/mcp", { method: "GET" })) (default handler via service binding) |
β
~121ms |
β
~0ms |
fetch("https://target.example.workers.dev/mcp", { headers: { ... } }) (public URL) |
β
~238ms |
β
~11ms |
(The 4xx/5xx on the fetch variants are downstream of the transport β they hit an OAuth-protected route with no bearer, just to verify request/response flow. With a route purpose-built for the use case they'd be 200s.)
Expected behavior
binding.callTool(...) on call #1 either resolves with the method's return value or rejects with an error, same as call #0.
Actual behavior
The Promise returned by call #1 never settles. .then and .catch are never invoked. Diagnostic logging on the caller side confirms the Promise is dispatched but no resolution/rejection callback ever fires.
Workaround
Exposing the same logic as an HTTP route on the WorkerEntrypoint's fetch method and switching the caller from binding.callTool(...) to binding.fetch(new Request(...)) over the same service binding eliminates the hang. Trust boundary is preserved (service-binding-only routing).
Notes
- Single-tenant CF account, paid plan.
- Reproduces 100% of the time across attempts.
- Same shape works fine when invoked via the public hostname or via the binding's default fetch handler β only the RPC dispatch path to a
WorkerEntrypoint method exhibits the hang.
- Happy to share a minimal standalone repro repo on request.
Summary
In a single Durable Object method, the second back-to-back call to a
WorkerEntrypointRPC method over a service binding (env.MY_BINDING.someMethod(...))returns a Promise that never settles β it neither resolves nor rejects.
The first call returns normally (~200ms). The second hangs until the
caller-side timeout fires. The hang is independent of:
Switching the transport on the same service binding to fetch
(
env.MY_BINDING.fetch(new Request(...))) makes the bug disappear β bothback-to-back calls return cleanly. So this is specific to the RPC
dispatch path, not the service binding itself.
Environment
workerd1.20260504.1(2026-04-29)compatibility_flags:["nodejs_compat"]Reproduction
Caller worker has a service binding pointed at a target
WorkerEntrypointnamed
TargetEntrypoint. Inside a DO method on the caller side, make twoback-to-back RPC calls:
The
TargetEntrypointside is a stockWorkerEntrypointsubclass:Transport matrix (same service binding, same target, two back-to-back calls)
binding.callTool({entraOid}, {name, arguments})(RPC entrypoint)binding.fetch(new Request("https://target/mcp", { method: "GET" }))(default handler via service binding)fetch("https://target.example.workers.dev/mcp", { headers: { ... } })(public URL)(The 4xx/5xx on the fetch variants are downstream of the transport β they hit an OAuth-protected route with no bearer, just to verify request/response flow. With a route purpose-built for the use case they'd be 200s.)
Expected behavior
binding.callTool(...)on call #1 either resolves with the method's return value or rejects with an error, same as call #0.Actual behavior
The Promise returned by call #1 never settles.
.thenand.catchare never invoked. Diagnostic logging on the caller side confirms the Promise is dispatched but no resolution/rejection callback ever fires.Workaround
Exposing the same logic as an HTTP route on the
WorkerEntrypoint'sfetchmethod and switching the caller frombinding.callTool(...)tobinding.fetch(new Request(...))over the same service binding eliminates the hang. Trust boundary is preserved (service-binding-only routing).Notes
WorkerEntrypointmethod exhibits the hang.