Skip to content

Real-time Streaming

EdgeFlags provides two WebSocket endpoints for real-time data:

EndpointPurposePermission
/stream/flagsReceive flag/config updates as they happenread:flags
/stream/tailStream individual evaluation events (admin live tail)* (admin)

Both endpoints authenticate via ?token= query parameter or Authorization: Bearer header.

Flag sync (/stream/flags)

The flag sync stream delivers flag and config changes to SDK clients in real-time, replacing polling with sub-50ms push updates.

Connection

const ws = new WebSocket('wss://edgeflags.net/stream/flags?token=ff_prod_abc123');

Client messages

After connecting, send a subscribe message to start receiving updates:

{
"type": "subscribe",
"env": "production",
"context": {
"user_id": "u_42",
"plan": "premium",
"custom": { "country": "US" }
}
}
MessageDescription
subscribeStart receiving updates for the given environment and context
update-contextChange the evaluation context (triggers a new snapshot)
pingKeep-alive; server responds with pong

Server messages

MessageDescription
snapshotFull set of evaluated flags and configs
diffIncremental changes since the last snapshot
pongResponse to ping
errorError with message and code fields

Snapshot

Sent after subscribe or update-context:

{
"type": "snapshot",
"flags": { "dark_mode": true, "new_checkout": false },
"configs": { "api_limits": { "requests_per_minute": 500 } }
}

Diff

Sent when a flag or config changes on the server:

{
"type": "diff",
"changes": [
{ "kind": "flag", "key": "dark_mode", "value": false },
{ "kind": "config", "key": "api_limits", "value": { "requests_per_minute": 1000 } }
]
}

SDK integration

The @edgeflags/sdk supports WebSocket mode. When the server has the FLAG_SYNC binding configured, the SDK can use streaming instead of polling:

const ef = new EdgeFlags({
token: 'ff_prod_abc123',
baseUrl: 'https://edgeflags.net',
context: { user_id: 'u_42', custom: {} },
streaming: true, // Use WebSocket instead of polling
});
await ef.init();

When streaming: true, the SDK opens a WebSocket to /stream/flags, subscribes with the current context, and applies diffs as they arrive. The change event fires on each diff, and useFlag/useConfig hooks re-render automatically.


Live evaluation tail (/stream/tail)

The live tail stream broadcasts individual flag evaluation events in real-time. This powers the Live Tail tab in the admin dashboard’s Observability page.

Connection

const ws = new WebSocket('wss://edgeflags.net/stream/tail?token=ff_prod_admin_token');

Client messages

MessageFieldsDescription
startfilters?Begin streaming; optionally filter by flag keys or reasons
filterfiltersUpdate active filters without reconnecting
pauseTemporarily stop receiving events
resumeResume after pause

Filters

{
"type": "start",
"filters": {
"flags": ["dark_mode", "new_checkout"],
"reasons": ["targeting", "rollout"]
}
}

Both filter fields are optional. When set, only matching events are delivered. Filter values:

  • flags — array of flag key strings
  • reasons — array of evaluation reasons: disabled, override, targeting, rollout, default, environment_override

Server messages

MessageDescription
backfillLast 50 matching events from the ring buffer (sent on connect)
evalIndividual evaluation event
statsPeriodic stats (total events, events/second rate) sent every 5s

Eval event

{
"type": "eval",
"ts": "2026-02-16T14:30:00.123Z",
"flag": "dark_mode",
"reason": "targeting",
"ms": 2.3,
"actor": "ff_prod_a1b2...",
"value": true,
"ctx": "user=u_42 plan=premium env=production"
}
FieldDescription
tsISO 8601 timestamp
flagFlag key that was evaluated
reasonEvaluation reason
msEvaluation duration in milliseconds
actorTruncated token prefix of the caller
valueEvaluated flag value
ctxCompact summary of the evaluation context (max 100 chars)

Backfill

Sent immediately after a start message:

{
"type": "backfill",
"events": [
{ "ts": "...", "flag": "dark_mode", "reason": "targeting", "ms": 1.2, "actor": "...", "value": true, "ctx": "..." }
]
}

Stats

Broadcast every 5 seconds to all connected viewers:

{
"type": "stats",
"total": 15234,
"rate": 42.5
}

Architecture

The live tail uses a dedicated Durable Object (TailRoomDO) separate from the flag sync DO. Evaluation routes emit events via waitUntil() with zero overhead when no viewers are connected. The DO maintains a 500-event ring buffer and broadcasts to all connected admin viewers with per-connection filtering.

Evaluation request → Worker → waitUntil(emit) → TailRoomDO → broadcast → Admin UI

Connection lifecycle

Both WebSocket endpoints follow the same connection state machine:

StateDescription
disconnectedNo active connection. Initial state or after all retries exhausted.
connectingWebSocket handshake in progress.
connectedHandshake complete, messages flowing.
reconnectingConnection lost, attempting to re-establish.

State transitions

disconnected → connecting → connected
reconnecting → connecting → connected
disconnected (max retries)

The SDK and admin dashboard manage these transitions automatically. On a successful connection, the client sends a subscribe (flag sync) or start (live tail) message to begin receiving data.

When the connection drops, the client moves to reconnecting and begins the backoff sequence. On reconnection, the client re-subscribes with the same parameters, and the server sends a fresh snapshot or backfill to ensure the client has current state.

Reconnection

Reconnection uses exponential backoff with a cap:

AttemptDelay
11 s
22 s
34 s
48 s
5+16 s (cap)

The backoff counter resets to zero after a successful connection that lasts more than 30 seconds. If the server returns a 4xx status during the WebSocket handshake (e.g. 401 Unauthorized), reconnection stops immediately since retrying would not help.

Server-side architecture

Both streaming endpoints use the Cloudflare Hibernatable WebSocket API. This means idle connections consume zero CPU — the Durable Object hibernates when no messages are in flight and wakes only when a message arrives or a broadcast is triggered.

Per-connection state (subscribed environment, active filters, pause state) is stored using serializeAttachment / deserializeAttachment on each WebSocket, so it survives hibernation without keeping the DO in memory.

Durable ObjectEndpointPurpose
FlagSyncDO/stream/flagsMaintains evaluated flag state, computes and pushes diffs on change
TailRoomDO/stream/tailBroadcasts evaluation events, manages 500-event ring buffer

When no viewers are connected to TailRoomDO, the waitUntil(emit) call in the evaluation path short-circuits via a connection count check, adding zero latency to normal flag evaluations.