Skip to content

Cloudflare: Broadcast

A stream fence declares the keys it listens on with tags:. broadcast(tag, json) is the server-side half: call it after the state behind a tag changes, and every open stream subscribed to that tag receives the frame — instantly, event-driven, no polling.

import { broadcast } from '@madenowhere/phaze-cloudflare/transport'

It is the streaming sibling of revalidateTag: one is cache invalidation, the other is connection invalidation.

A held SSE/WebSocket connection lives in one Workers isolate. The request that changes the state behind it — a logout POST, a webhook, an admin action — is routed independently and routinely lands in a different isolate, often a different colo or machine.

isolate A isolate B
───────── ─────────
GET /transport/stream/Session POST /transport/action/Session/delete
↓ ↓
connection held open ←── ??? ─── broadcast('session:<token>', 'null')

In-process memory cannot cross that gap, and storage cannot either — a KV or D1 write can’t wake another isolate’s live connection. The only alternative would be the held connection polling its own store, which is both slow and, in local dev, defeated by per-isolate read caching.

So the need is a push rendezvous, not a data store: a single, globally addressable meeting point per tag that any isolate can reach.

broadcast() resolves that rendezvous with a Durable Object keyed on the tag:

  • One DO instance per tag — idFromName(tag). From any isolate, colo, or machine, the same tag resolves to the same object.
  • Subscribe — when a stream connection opens, the worker opens a WebSocket to that DO and relays every message it receives into the connection’s own frame pipeline (so sign: / encrypt: still apply).
  • Broadcast — broadcast(tag, json) POSTs to the tag’s DO, which fans out to every socket it holds.
  • Hibernation — sockets are accepted with the Hibernation API, so the DO can be evicted while connections stay held; a later broadcast wakes it and the sockets are still there. An idle held connection doesn’t bill DO duration.

The DO stores nothing. It’s used for its addressability — a canonical instance per name — not its durability.

Declare tags: on the stream fence, then broadcast to the same key from anywhere on the server:

---Session
transport: sse
use: [session]
out: Json<>
tags: [`session:${cookies.get(COOKIE)}`]
---
return session
// in an action, provider, or endpoint — anywhere with server context
await transport.SESSIONS.delete(`session:${token}`)
await broadcast(`session:${token}`, 'null') // every live Session stream receives null

Both stream wires are supported — transport: sse and transport: ws subscribe through the same path.

Await it. On the Durable Object path broadcast() returns a promise for the cross-isolate fan-out; awaiting it means delivery completes before your response ships. On the in-memory path it resolves synchronously, so awaiting is a no-op — callers can always await.

The payload is a string. broadcast(tag, json: string) carries an opaque string; the receiving stream applies its own out: shape and per-frame crypto. Binary frames (out: Flat<T>) must be encoded by the caller.

broadcast() is opt-in by binding. Declare the Durable Object in your wrangler.jsonc:

{
"durable_objects": {
"bindings": [{ "name": "PHAZE_BROADCAST", "class_name": "PhazeBroadcast" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["PhazeBroadcast"] }]
}

That’s the whole setup. The PhazeBroadcast class is exported from the generated worker entry unconditionally, so wrangler can always resolve class_name — the framework never reads your config, and the binding’s presence at runtime is the only signal. Declare it once, in the place Cloudflare already requires it.

APIbroadcast(tag: string, json: string): void | Promise<void>
Import@madenowhere/phaze-cloudflare/transport
Paired withthe tags: knob on an sse / ws stream fence
Backed byone PhazeBroadcast Durable Object per tag (idFromName)
Requiresthe PHAZE_BROADCAST binding + migration in wrangler.jsonc
Without the bindingin-process registry, same-isolate delivery only

See also: Phaze Transport for the tags: knob and stream fences.