cloudflare / cloudflare/workerd
WorkerEntrypoint RPC crashes (wallTime=0) when worker co-exports a DurableObject class
- Dominant language
- C++
- Stars
- 8.7k
- Forks
- 739
- Avg merge
- 2d 20h
- Merged PRs (30d)
- 174
Description
## Environment
- **wrangler**: 4.77.0
- **compatibility_date**: 2025-09-15
- **compatibility_flags**: `["nodejs_compat"]`
- **Platform**: Cloudflare Workers (staging deployment)
## Summary
When a Worker exports both a `WorkerEntrypoint` subclass and a `DurableObject` subclass, any RPC call to the `WorkerEntrypoint` crashes immediately with `wallTime=0 cpuTime=0 outcome=exception/canceled`. The same Worker responds correctly when invoked via HTTP `fetch()`.
**This is NOT specific to any one package.** We have confirmed the crash with two completely independent packages:
| Worker | Package | Depends on containers? | RPC Crash? |
|--------|---------|----------------------|------------|
| sandbox | `@cloudflare/sandbox` v0.8.0 | Yes | wallTime=0, outcome=canceled |
| browsing | `@cloudflare/puppeteer` v0.0.15 | **No** | wallTime=0, outcome=exception |
The common pattern: **WorkerEntrypoint + co-exported DurableObject class + RPC invocation**.
## Confirmed Staging Log Evidence
### Case 1: @cloudflare/sandbox — RPC crashes, HTTP works
**RPC call (crashes):**
```json
{
"entrypoint": "SandboxService",
"rpcMethod": "exec",
"wallTime": 0,
"cpuTime": 0,
"outcome": "canceled",
"scriptName": "cccc-sandbox-staging"
}
```
**HTTP fetch to same worker (works):**
```json
{
"event": { "request": { "url": ".../health", "method": "GET" } },
"wallTime": 1,
"outcome": "ok",
"scriptName": "cccc-sandbox-staging"
}
```
### Case 2: @cloudflare/puppeteer — Same crash, no containers dependency
```json
{
"entrypoint": "BrowsingService",
"rpcMethod": "navigate",
"wallTime": 0,
"cpuTime": 0,
"outcome": "exception",
"scriptName": "cccc-browsing-staging"
}
```
`@cloudflare/puppeteer` has zero dependency on `@cloudflare/containers`. Its dependencies are: `chromium-bidi`, `cross-fetch`, `debug`, `devtools-protocol`, `ws`, `@puppeteer/browsers`. This proves the crash is in workerd's RPC module initialization, not in any specific package.
## Steps to Reproduce
### Minimal setup
**Worker B** (target) — exports both a DO and a WorkerEntrypoint:
```ts
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
// Any DurableObject subclass
export class MyDO extends DurableObject {
async sayHello() { return "hello from DO"; }
}
// WorkerEntrypoint that other workers call via RPC
export default class MyService extends WorkerEntrypoint {
async greet(name: string): Promise {
return `Hello, ${name}!`; // Never executes — crash happens before this
}
}
```
```toml
# wrangler.toml
name = "worker-b"
main = "src/index.ts"
compatibility_date = "2025-09-15"
[[durable_objects.bindings]]
name = "MY_DO"
class_name = "MyDO"
[[migrations]]
tag = "v1"
new_classes = ["MyDO"]
```
**Worker A** (caller) — calls Worker B via RPC:
```ts
export default {
async fetch(request: Request, env: any): Promise {
const result = await env.WORKER_B.greet("world");
return new Response(result);
},
};
```
```toml
# wrangler.toml
name = "worker-a"
main = "src/index.ts"
[[services]]
binding = "WORKER_B"
service = "worker-b"
entrypoint = "MyService"
```
### What happens
1. Worker A sends RPC call to `env.WORKER_B.greet("world")`
2. Worker B crashes: `wallTime=0 cpuTime=0 outcome=exception`
3. Worker A receives opaque internal error
4. No user code in Worker B executes
### Contrast: HTTP fetch works
`env.WORKER_B.fetch(new Request("https://fake/health"))` succeeds. The crash is specific to the RPC invocation path.
## Root Cause Analysis
The crash happens during **workerd's module initialization for RPC dispatch**. When workerd receives an RPC call targeting a `WorkerEntrypoint`, it evaluates the target Worker's module graph to discover and validate exported classes. This evaluation triggers something in the DurableObject subclass definitions that crashes in the RPC initialization context.
Key observations:
- `wallTime=0` confirms crash is during module init, not method execution
- The crash occurs regardless of which `WorkerEntrypoint` method is called
- The crash occurs even if the DO class is never instantiated or referenced by the entrypoint
- HTTP fetch uses a different init path that doesn't perform the same class validation
- Two unrelated packages (`@cloudflare/sandbox`, `@cloudflare/puppeteer`) both trigger it, ruling out package-specific causes
**The mere presence of a DurableObject subclass export in the same module graph as a WorkerEntrypoint is sufficient to trigger the crash.**
## Expected Behavior
RPC calls to a `WorkerEntrypoint` should succeed regardless of what other classes (including `DurableObject` subclasses) are exported from the same module. Class definitions that are not being instantiated should be inert during module evaluation.
## Workaround (Confirmed Working)
Convert the affected Worker from `WorkerEntrypoint` RPC to plain `export default { fetch() {} }` with HTTP fetch routing. The calling Worker uses `env.BINDING.fetch()` with JSON payloads instead of RPC.
```ts
// Instead of WorkerEntrypoint RPC:
export default class MyService extends WorkerEntrypoint { ... }
// Use plain module export with fetch routing:
export default {
async fetch(request: Request, env: Env): Promise {
const { method, args } = await request.json();
// Route to handlers based on method...
},
};
```
This works but loses RPC's type safety, requires manual JSON serialization, and adds unnecessary complexity.
## Impact
This bug effectively makes **WorkerEntrypoint RPC unusable for any Worker that also uses Durable Objects**. Since DO-based Workers are a core Cloudflare pattern (Containers, Browser Rendering, custom DOs), this forces a large class of Workers into the HTTP fetch workaround, negating the benefits of the RPC API.
## Affected Packages (confirmed)
- `@cloudflare/sandbox` v0.8.0 (exports `Sandbox extends Container extends DurableObject`)
- `@cloudflare/puppeteer` v0.0.15 (exports `BrowserSessionDO extends DurableObject` pattern)
- Likely any Worker that co-exports `WorkerEntrypoint` + `DurableObject` subclasses
## Related
- Workers RPC docs: https://developers.cloudflare.com/workers/runtime-apis/rpc/
- Durable Objects docs: https://developers.cloudflare.com/durable-objects/
- Cloudflare Containers: https://developers.cloudflare.com/containers/
- Browser Rendering API: https://developers.cloudflare.com/browser-rendering/
- Related issue: #4499 (DO RPC method override issue — different but adjacent)
Contributor guide
Assessment
This issue has not been assessed yet.