cloudflare / cloudflare/agents
Facet broadcasts after the frame completes cost two root RPCs per message (getAgentByName on every broadcast)
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 711
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 53
Description
## Summary
When a facet broadcasts after the inbound frame that started its work has completed, every broadcast costs two RPC calls to the root: `__unsafe_ensureInitialized` (from `getAgentByName`) and `_cf_broadcastToSubAgent`. A streamed chat turn broadcasts once per stream chunk, so one long answer makes thousands of billed Durable Object requests to the root.
Seen on `agents@0.22.0`. The same code path is still there in `0.23.0`.
## Where
`src/dynamic-agents/dynamic-agents.ts` (0.23.0):
- `routeBroadcast()` (~line 824) uses the live frame bridge only while the frame's `AsyncLocalStorage` context is active and no older operation is pending. Otherwise it calls `(await this.rootAlarmOwner())._cf_broadcastToSubAgent(...)`.
- `rootAlarmOwner()` (~line 216) calls `getAgentByName(binding, root.name)` every time. `getAgentByName` always awaits `stub.__unsafe_ensureInitialized()` before it returns the stub.
A chat turn keeps streaming after the user's frame is acknowledged, so for the whole model stream each chunk takes the root path. That means one `ensureInitialized` RPC and one broadcast RPC per chunk.
## Evidence
Setup: a root `Agent` (the user's directory) with `Think` chat facets, connected through `useAgent({ sub: [...] })`. `wrangler tail` on a deployed Worker during about three streamed answers:
```
2656 HalleyDirectory __unsafe_ensureInitialized
2611 HalleyDirectory _cf_broadcastToSubAgent
16 HalleyDirectory _cf_setSubAgentConnectionState
13 HalleyAgent _cf_initAsFacet
```
The two counts track each other almost 1:1.
## Impact
- Each DO RPC method call is a billed request ([pricing](https://developers.cloudflare.com/durable-objects/platform/pricing/)). The paid plan's 1M included requests cover only a few hundred long answers a month.
- With Workers traces on, each call emits spans. Spans become billed observability events on October 1, 2026.
- Each chunk waits on two serialized cross-object round trips. The broadcast tail makes them sequential.
## Suggestion
1. Resolve the root stub once per facet instance and reuse it in `rootAlarmOwner()`. By the time a facet is broadcasting, the root has already initialized it, so running `ensureInitialized` again on every call looks redundant. This alone would halve the calls.
2. Optionally, coalesce facet broadcasts on the root path, for example by batching messages queued behind the same tail into a single `_cf_broadcastToSubAgent` call.
Happy to test a patch.
Contributor guide
Research direction
Start in src/dynamic-agents/dynamic-agents.ts by reading routeBroadcast(), rootAlarmOwner(), and getAgentByName(), then reproduce the streamed-broadcast behavior with the wrangler tail evidence described in the issue. Determine how the root stub can be reused without changing broadcast behavior, and verify that repeated chunks no longer cause a matching __unsafe_ensureInitialized call for every broadcast.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100