Live Data: WebSockets & Streams
Advanced · 20 min read · ▶ live playground · ✦ checkpoint
Most web screens do not need real-time data. They need an honest freshness promise: "refresh when the user asks," "check every few seconds," "listen for server updates," or "keep a two-way connection open." Pick the simplest model that matches the user problem.
This lesson closes the server section by comparing the ladder: polling, server-sent events, WebSockets, and streams. You will not open a real WebSocket here; the live demo uses a local ReadableStream so you can practice consuming chunks without network randomness.
Polling is asking again on a schedule
Polling means sending a normal request again and again. You already know the request shape from fetch: ask the server for the current state, render it, wait, ask again.
async function pollOrders() {
const response = await fetch("/api/orders");
if (!response.ok) {
renderError(`HTTP ${response.status}`);
return;
}
const orders = await response.json();
renderOrders(orders);
}
pollOrders();
const pollingId = setInterval(pollOrders, 10_000);
// Later, when the screen closes:
clearInterval(pollingId);Polling is easy to build and easy to debug because each update is just another HTTP request. It works well for dashboards, admin screens, inbox counts, build status, and anything where "fresh within a few seconds" is good enough.
The tradeoff is waste. If nothing changed, the client still asks. If many clients poll too often, the server answers many duplicate requests. Start with polling when the product can tolerate a small delay, then escalate only when the delay or waste becomes a real problem.
SSE is server-to-browser updates
Server-sent events, usually called SSE, let the browser open one HTTP connection and receive text events from the server over time. It is one-way: server to browser.
const events = new EventSource("/api/order-events");
events.addEventListener("message", (event) => {
const update = JSON.parse(event.data);
renderOrderUpdate(update);
});
events.addEventListener("error", () => {
renderConnectionProblem();
});
// Later, when the screen closes:
events.close();SSE fits live logs, notification feeds, deployment progress, stock tickers where the browser only listens, and dashboards where the client does not need to send frequent messages back through the same pipe.
Compared with polling, SSE avoids repeated "anything new?" requests. Compared with WebSockets, it is simpler when the data flows mostly one way.
WebSockets are two-way conversations
WebSockets keep one connection open so the browser and server can both send messages whenever they need to. That makes them the right tool for chat, multiplayer collaboration, shared cursors, live games, trading interfaces, and other truly two-way experiences.
WebSocket examples in this lesson are static on purpose. Real sockets need a real server that speaks the protocol, plus reconnection, authentication, and cleanup rules.
const socket = new WebSocket("wss://example.test/live");
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ type: "join", room: "orders" }));
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
renderLiveMessage(message);
});
socket.addEventListener("close", () => {
renderDisconnected();
});
// Later, when the screen closes:
socket.close();The upside is speed and two-way flow. The cost is more state: connected, disconnected, reconnecting, authenticated, subscribed, stale, duplicate messages, missed messages. If the page only needs to refresh a list every 30 seconds, WebSockets are probably too much.
Streams deliver chunks over time
A stream is data that arrives in pieces. Instead of waiting for one complete body, your code consumes chunks as they become available. Network responses can be streams, but streams are a broader browser primitive too.
The core reading loop is:
const reader = stream.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
renderChunk(value);
}reader.read() gives one chunk at a time. When the stream is finished, done becomes true. That shape should feel familiar from async iteration: work arrives over time, and your UI updates as each piece appears.
Try it — consume a local stream
This playground creates a ReadableStream inside the browser. There is no network request. The stream emits deterministic chunks on distinct timer delays, then closes. Click Cancel stream before it finishes to see cleanup.
The code never assumes all data exists up front. It renders each chunk as the stream produces it, then handles the close or cancel path. That same mental model applies when a real response body streams a large file, a generated answer, or progress output.
Choose the simplest honest freshness model
The ladder is a decision tool:
- Manual refresh — best when stale data is harmless and the user controls the moment.
- Polling — best when "fresh every few seconds" is honest enough.
- SSE — best when the server pushes one-way updates to the browser.
- WebSockets — best when both sides need to send messages at any moment.
- Streams — best when one response produces useful chunks before the whole result is complete.
Most apps should start lower on the ladder. A settings page, account screen, product list, or admin table usually does not need real-time infrastructure. A chat room, collaborative editor, multiplayer board, live incident feed, or streaming generation UI might.
The next section shifts from talking to servers to keeping browser apps organized: storage, state, rendering, and URL-driven UI. The server tells you what changed; your app state decides what the page should show.
Checkpoint
Answer all three to mark this lesson complete