Stream on Cloudflare
Environment: server.
Beta — Telefunc Stream is in beta: breaking changes may occur in any version update.
Use Telefunc Stream on Cloudflare (its Cloudflare Workers platform).
Setup
// worker.js
// Environment: server
import { Telefunc } from 'telefunc/cloudflare'
const telefunc = new Telefunc()
export const TelefuncDurableObject = telefunc.TelefuncDurableObject
export default {
async fetch(request, env, ctx) {
const response = await telefunc.serve({ request, env, ctx })
if (response) return response
return new Response('Not found', { status: 404 })
}
}// worker.ts
// Environment: server
import { Telefunc } from 'telefunc/cloudflare'
const telefunc = new Telefunc()
export const TelefuncDurableObject = telefunc.TelefuncDurableObject
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const response = await telefunc.serve({ request, env, ctx })
if (response) return response
return new Response('Not found', { status: 404 })
},
}// wrangler.jsonc
{
"durable_objects": {
"bindings": [
{ "name": "TelefuncDurableObject", "class_name": "TelefuncDurableObject" }
]
},
"kv_namespaces": [
{ "binding": "TelefuncKV", "id": "your-kv-namespace-id" }
],
"migrations": [
{ "tag": "v1", "new_classes": ["TelefuncDurableObject"] }
]
}Fundamentally, channels are stateful: the server holds live state for each open channel. Cloudflare Workers, however, are stateless and ephemeral — any request can be served by any worker, and nothing is remembered between requests. The state therefore needs to live somewhere else — Durable Objects and KV are Cloudflare's primitives for exactly that:
- Durable Objects provide stateful compute: each channel lives on a Durable Object, which holds the connection and its state.
- KV provides shared storage: it's how stateless workers find the Durable Object that holds the state, no matter which worker a request lands on.
Both bindings are required whenever you use
telefunc/cloudflare. If your app doesn't useChannelorBroadcastChannel, there's no state to keep — you can skip both bindings and useserve()instead.See Architecture for implementation details.
Context
Pass per-request data to telefunctions via getContext():
const telefunc = new Telefunc({
context: async (request, env) => ({
user: await getUserFromCookie(request, env)
})
})const telefunc = new Telefunc({
context: async (request, env: Env) => ({
user: await getUserFromCookie(request, env)
})
})See: API > new Telefunc().
Architecture
Telefunc uses Durable Objects for channel state and broadcast fan-out. Channel state is in-memory JavaScript (closures, callbacks, local variables), so it must live on the same Durable Object that holds the WebSocket connection.
KV stores two things:
- Session tokens — pin a browser to the same Durable Object across requests.
- Broadcast presence — tracks which Durable Objects have active subscribers for each key.
Regions
Telefunc maps each request to one of six geographic regions using Cloudflare's colocation data:
| Region | Code |
|---|---|
| West North America | wnam |
| East North America | enam |
| West Europe | weur |
| East Europe | eeur |
| Asia Pacific | apac |
| Oceania | oc |
Each region runs its own Durable Objects so that channel state stays close to users.
Session affinity
Every telefunction call and channel message from a client must reach the same Durable Object — otherwise that client's channel state would be unavailable.
On the first request, Telefunc picks a Durable Object in the nearest region, stores a session token in KV (TTL: 24 hours), and returns that token to the client via the x-telefunc-session header. Subsequent requests send this token back so Telefunc routes to the same Durable Object.
⚠️CDN / reverse proxy — make sure
x-telefunc-sessionisn't stripped from responses or requests. Without it, each request would be routed to a random Durable Object and channel state would be lost.
Scaling
By default, Telefunc creates one Durable Object per region. Increase capacity with scale:
const telefunc = new Telefunc({ scale: 4 })This creates 4 Durable Objects per region, and Telefunc distributes sessions across them.
Per-region scale
const telefunc = new Telefunc({
scale: { weur: 5, enam: 3, apac: 2 }
})const telefunc = new Telefunc({
scale: { weur: 5, enam: 3, apac: 2 },
})Only specified regions get Durable Objects. Requests from unspecified regions fall back to locationFallback.
Distributed broadcast
Broadcasts fan out across all regions automatically.
How it works
🧠You can skip this section — it's Telefunc's internal fan-out, not anything you configure.
Publisher (Durable Object)
→ Authority DO (one per key — assigns sequence numbers)
→ Coordinators (one per active region)
→ Durable Objects (deliver to local subscribers)
- The publisher forwards the message to an authority Durable Object for the key.
- The authority assigns a monotonic sequence number, reads active presence from KV, and forwards to each region with subscribers.
- Each region's coordinator fans out to the Durable Objects in that region.
- Each Durable Object delivers to its local subscribers.
Ordering
Broadcast publishes for a given key go through a single authority, so subscribers receive messages in order with a monotonic seq.
Presence
Telefunc uses KV to track which Durable Objects have active subscribers:
| Setting | Value |
|---|---|
| TTL | 90 seconds |
| Refresh | every 30 seconds |
A KV record is created on subscribe and deleted on unsubscribe. If a Durable Object is evicted (e.g. during a deployment), the record expires after 90 seconds and the region is excluded from fan-out.
Delivery guarantees
Channel messages (send / listen)
| Guarantee | Details |
|---|---|
| Ordering | Messages are delivered in order. |
| Buffering | The server buffers messages while the client is disconnected: up to config.channel.bufferLimit for text (default: 512 KB) and up to config.channel.bufferLimitBinary for binary (default: 2 MB). Binary frames have a separate budget and can never evict text. |
| Replay | Both sides keep a replay buffer. On reconnect, missing messages are replayed and duplicates are ignored. |
| Acknowledgements | send(data, { ack: true }) resolves when the other side processes the message. |
| Loss | If the disconnection lasts longer than config.channel.reconnectTimeout, the channel closes with NetworkError. |
Broadcast messages (publish / subscribe)
| Guarantee | Details |
|---|---|
| Ordering | Publishes for a given key are serialized and delivered in order. |
| Delivery | publish() resolves after all Durable Objects with subscribers have received the message. Delivery to clients then happens over the channel connection. |
| Presence lag | A new subscriber may miss publishes during the few milliseconds it takes to write its KV presence record. |
Once a broadcast message reaches a Durable Object, it has the same buffering and replay guarantees as regular channel messages.
Configuration
const telefunc = new Telefunc({
bindingName: 'TelefuncDurableObject', // DO binding name (default)
kvBindingName: 'TelefuncKV', // KV binding name (default)
instanceName: 'telefunc', // Base name for DO instances (default)
scale: 1, // DOs per region (default)
locationFallback: 'weur', // Fallback region (default)
jurisdiction: 'eu', // Restrict DOs to a jurisdiction (optional, no default)
})See also: API > new Telefunc().
Hibernation
Hibernation while channels are open isn't supported.
Channel state is in-memory JavaScript. If the Durable Object hibernates, that state is gone. Telefunc keeps the Durable Object alive while channels are open.
The Durable Object can hibernate once all channels are closed, no clients remain connected, and both the reconnect and idle windows have expired.
import { config } from 'telefunc'
// Shorter windows = hibernate sooner, less disconnect tolerance
config.channel = {
reconnectTimeout: 5_000,
idleTimeout: 5_000,
pingInterval: 2_000
}