cloudflare / cloudflare/agents

Facet broadcasts after the frame completes cost two root RPCs per message (getAgentByName on every broadcast)

Open
#2,252 1 comment 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.