Real-Time Data Delivery to Web UIs — SSE and WebSocket at the Edge, the Reconnection-and-Resume Contract, and Backpressure for Streaming Frontends
A streaming UI's job is not to be fast. It is to never lie about how fresh it is.
§ IFrame
A request/response API answers a question the client asked. A streaming channel answers a question no one asked yet: what changed since the last time you looked. The two are different services with different failure modes, and a frontend that treats the second like the first will ship a UI that lies about how fresh its data is.
The 06-28 lesson named the request/response contract: shape, access, error envelope. That contract governs the pull. This lesson governs the push. When ETF flow data updates twelve times a minute and a market UI must reflect each change without the user clicking refresh, the backend holds a connection open and writes down it. Holding a connection open is cheap to describe and expensive to operate. The connection drops on a train, on a laptop lid, on a load-balancer timeout, on a deploy. The question is never whether it drops. The question is what the frontend does in the second after it drops, and whether the user can tell.
This lesson names the two push transports, specifies the reconnection-and-resume contract that keeps a dropped stream honest, and installs the backpressure discipline that keeps a fast producer from drowning a slow browser.
§ IIFoundations
Two transports, one decision
The browser ships two mechanisms for server-pushed data. They are not interchangeable, and the choice is an architecture decision, not a preference.
Server-Sent Events (SSE) is a one-way channel: server to client, text only, over a single long-lived HTTP response. The browser's EventSource opens the connection, and the server writes data: lines down it as events arrive. Flanagan is precise about the shape of the wire: the server sets Content-Type: text/event-stream, keeps the connection open rather than calling response.end(), and formats each message as one or more data: lines terminated by a blank line. The connection is an ordinary HTTP response that never finishes.
WebSocket is a two-way channel: full-duplex, binary or text, over a connection that starts as HTTP and upgrades to the ws:// protocol. Either side may send at any time. The browser's WebSocket object exposes send() and an onmessage handler.
The decision rule is direction. If the data flows one way — server to UI, a market feed, a notification stream, a progress bar — SSE is the smaller tool and the right one. It rides plain HTTP, so it crosses proxies and CDNs that mangle protocol upgrades, and EventSource reconnects on its own. If the UI must send as fast as it receives — a collaborative editor, a trading terminal that fires orders, a chat with typing indicators — WebSocket earns its extra operational weight.
Most market-data UIs are one-way. Most teams reach for WebSocket anyway, out of habit, and inherit a heavier operational surface than the problem needed. Choose the channel by the direction of the data, not by which name sounds more capable.
The freshness contract
A streaming UI makes a promise the pull UI never made: what you see is current. That promise has a failure mode the pull UI does not: the connection can be dead while the last-painted value sits on screen looking alive. A number that stopped updating four minutes ago is more dangerous than a number that never loaded, because the user trusts it.
So the freshness contract has two clauses. The data clause: every event carries the time it was true, an as_of_ms on the wire. The liveness clause: the UI knows whether the channel is currently connected, and shows that state, so a frozen feed reads as frozen rather than as calm.
§ IIIMechanism
The reconnection-and-resume contract
A dropped connection is the normal case. The contract that governs recovery has three parts, and EventSource gives you the first for free while leaving the other two as work you must do.
The first part is reconnect. EventSource reconnects on its own after a drop, waiting a few seconds and reopening the request. This is the piece teams assume is the whole job. It is not.
The second part is resume without a gap. When the connection reopens, the server has no idea what the client already saw. Without coordination, the client either misses the events that arrived during the outage or receives the whole history again. SSE names the coordination mechanism: the id: field. The server tags each event with a monotonic id, and on reconnect the browser sends the last id it saw in the Last-Event-ID header. A correct server reads that header and replays only what came after. The id turns a blind reconnect into a resume from a known point. A server that ignores Last-Event-ID reconnects successfully and loses data anyway, which is worse than failing loudly, because the graph keeps drawing and the missing tick never shows.
The third part is bound the replay. A server cannot hold infinite history. It holds a window — the last N events, or the last few minutes — and when a client reconnects with a Last-Event-ID older than the window, the server cannot resume cleanly. The honest move is to send a resync signal telling the client to discard its local view and refetch a full snapshot from the pull API, then resume streaming from the current id. The resync path is where streaming and the 06-28 request/response contract meet: the snapshot comes from the pull endpoint, and the stream picks up from there.
Backpressure: the producer is faster than the browser
A server that can write a thousand events a second does not mean a browser that can render a thousand events a second. The main thread paints, runs the reconciliation, handles input. Push a thousand DOM-affecting events at it per second and the tab locks, the fans spin, the user watches a frozen page. This is backpressure: the point where a fast producer meets a slow consumer, and something has to give.
The wrong answer is to let the queue grow without bound. An unbounded buffer trades a frozen tab now for an out-of-memory crash later, and the events it holds are stale by the time they render anyway.
Three disciplines keep the producer inside the consumer's budget.
The edge: where streaming connections go to die
A streaming connection is a connection held open, and every layer between the server and the browser has an opinion about connections held open. The DevOps surface for streaming lives in those opinions.
A load balancer or reverse proxy has an idle timeout, and a stream that goes quiet during a lull looks idle. The server sends a heartbeat — an SSE comment line, a colon followed by a newline, every fifteen or twenty seconds — so the connection carries traffic even when no data changed, and the proxy leaves it alone. A CDN may buffer responses, which defeats streaming entirely by holding events until the buffer fills; the streaming route needs buffering disabled at the edge. HTTP/1.1 caps connections per host at six, so a page holding one SSE stream open has spent one of six; HTTP/2 multiplexes many streams over one connection and lifts the cap, which is why streaming at scale rides HTTP/2. And a deploy that drains connections (the 06-21 graceful-shutdown lesson) closes every open stream, so the client's reconnect-and-resume contract is what turns a routine deploy from an outage the user sees into a blip the user does not.
§ IVWorked Example
A market UI streams live ETF flow to a Three.js scene. The transport is SSE, because the flow is server-to-client only.
The server sets Content-Type: text/event-stream, disables proxy buffering on the route, and writes one event per ticker update:
id: 48213
event: flow
data: {"ticker":"IBIT","flow_1d_usd":-40400000,"as_of_ms":1751690400000}
Every fifteen seconds during a lull it writes a heartbeat comment so the proxy sees traffic:
: heartbeat
The client opens an EventSource, keeps a newest-per-ticker map, and never queues:
- On
flowevent: parse, write the value into the store under the ticker key, overwriting any unrendered prior value. The store is the sample; there is no queue to grow. - The render loop reads the store once per animation frame and updates bar heights. Sixty updates a second per bar, maximum, regardless of how fast events arrive.
- On
open: set the liveness state to connected. The scene shows a live indicator. - On
error: set the liveness state to reconnecting. The scene dims the live indicator and freezes bars at their last value, and the corner timestamp — driven byas_of_ms— stops advancing, so the user sees the feed is stale rather than trusting a frozen number.
When the connection drops on a laptop lid and reopens two minutes later, the browser sends Last-Event-ID: 48213. The server holds a five-minute window, so 48213 is still inside it; the server replays events 48214 through the current id, the client applies each into its store, and the next frame paints the caught-up state. Had the outage run past the window, the server would have sent a resync event; the client would have refetched the snapshot from the 06-28 pull endpoint, discarded its stale map, and resumed streaming from the current id.
EventSource gives you reconnect. The id: field and Last-Event-ID give you resume. The window and the resync signal give you the honest fallback when resume is impossible. A stream that only reconnects is a stream that silently loses data.
§ VConnection to Prior Lessons
The 06-28 API-contract lesson governs the pull; this lesson governs the push, and the two meet at the resync path. When a streaming client falls outside the replay window, it recovers by calling the pull endpoint for a fresh snapshot, then resuming the stream. Neither channel is complete alone. The pull answers what is true now; the push answers what changed; the resync is the client admitting it lost the thread and asking the pull to re-establish ground truth.
The 06-07 observability lesson named the degradation ladder: healthy, stale, degraded, disconnected, dark. The streaming liveness state is the wire that drives that ladder in real time. Connected with fresh as_of_ms is healthy. Connected with an as_of_ms that has stopped advancing is stale. Reconnecting is degraded. A reconnect loop that never succeeds is disconnected. The ladder was the concept; the SSE open and error events are the signal that moves the UI up and down it.
The 06-21 zero-downtime deploy lesson drained connections on SIGTERM. Every drain closes every open stream. The reconnection-and-resume contract is what makes that drain invisible: the client reconnects to the new instance, resumes from its last id, and the user sees a half-second dim instead of a gap.
§ VIConnection to Today's Dev Lesson
Today's Dev lesson builds the client half of this contract in React and Three.js. The decouple-receipt-from-render discipline named in §III is, in React terms, a useRef mutable store written by the EventSource handler and read by the requestAnimationFrame loop, deliberately outside React's render cycle so a hundred events a second never trigger a hundred re-renders. The Ops lesson names why the seam exists; the Dev lesson shows the exact hook boundary where it lives. Read together, they are the full path from the server's text/event-stream write to the bar that moves on screen.
§ VIIClosing
Choose the channel by the direction of the data. Carry the time-of-truth on every event. Show the liveness state so a dead feed reads as dead. Reconnect, resume from the last id, and resync from the pull API when resume is impossible. Decouple receipt from render so the producer's speed is never the renderer's problem.
A streaming UI's job is not to be fast. It is to never lie about how fresh it is.
Find one live-updating view in your product. Cut the network connection and watch it. If the last value sits there looking current, you have a UI that lies. Fix the liveness state first, before you optimize a single millisecond of latency.
Paired lessons → Dev Polyglot-Dev/Web/2026-07-05-streaming-live-market-data-into-react-and-three-js-... · Cert Cert-Prep/Anthropic/2026-07-05-streaming-claude-responses-to-production-uis-...
Filed 2026-07-05 Sunday Fajr · pure-DevOps Ops lesson · real-time-delivery arc rung 7
Backward-Synergy-Reach → API Contract (Sun 06-28) · Observability (Sun 06-07) · Zero-Downtime Deploys (Sun 06-21)
HTML shipped in-cycle per HARD DISCIPLINE · earth-accent meta-card · tome-grounded Flanagan §15.11.2-15.11.3