cloudflare / cloudflare/workerd
🐛 Bug Report — Runtime APIs: service-binding RPC dispatch hangs on second back-to-back call to WorkerEntrypoint
- Dominant language
- C++
- Stars
- 8.7k
- Forks
- 739
- Avg merge
- 2d 20h
- Merged PRs (30d)
- 174
Description
## 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:
```ts
// 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:
```ts
import { WorkerEntrypoint } from "cloudflare:workers";
export class TargetEntrypoint extends WorkerEntrypoint {
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.
Contributor guide
Assessment
This issue has not been assessed yet.