Skip to content

πŸ› Bug Report β€” Runtime APIs: service-binding RPC dispatch hangs on second back-to-back call to WorkerEntrypointΒ #6782

@toonverbeek

Description

@toonverbeek

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    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