Topic 16.3
Real-Time Updates
In one line
Live data raises four questions: how it arrives, in what order, how often to draw it, and what to do when nobody is looking or it stops. Apply messages at once, draw on a timer, ignore late ones, and clean up when you leave.
Think of it like this
The departure board in a train station. New information arrives all the time: a platform changes, a train is late. The board does not redraw for every whisper. It updates its rows a few times a minute, and if a message about the 9:10 train arrives after a newer one, the board does not go back in time.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- polling
- The page asks the server for the latest data again and again, on a timer.
- SSE
- Server-Sent Events: the server keeps one HTTP connection open and sends messages down it. The browser reconnects by itself.
- WebSocket
- A two-way connection between page and server. You write the reconnecting yourself.
- sequence number
- A number that grows with every message, so the page can tell which of two messages is newer.
- throttle
- Allow an action at most once per interval, however often it is asked for.
- rolling window
- The last N points (or last N minutes) of a stream; older ones are dropped.
- visibilitychange
- The event that fires when a tab becomes hidden or visible again.
document.hiddentells which.
Step by step
01Feed the rules every bad message
The first example sends applyEvent eight messages: a normal one, a late one, a duplicate, an over-capacity count, an unknown station, text that is not JSON, null and a count that is a string. It prints what each one did.
It then keeps the last five numbers of a stream in a rolling window.
02Send a burst, draw once
The second example starts the real status box with a simulated feed and sends 500 messages in one loop. The log shows the counts: most were applied, the late ones were ignored where something newer existed, and the box was drawn once at the start and once after the redraw interval.
03Hide the tab
The third example fakes a hidden tab with a flag, because a preview cannot really hide. Messages arrive and are applied, but the draw count does not move. When the flag turns back, one visibilitychange event draws once.
It ends with stop(): after it, more messages change nothing.
04Choose how the data gets in
The last code block shows the three real clients. For the status wall, SSE is the best fit: one-way, automatic reconnect, one line to start. Polling is the fallback behind proxies that cut long connections. Keep the makeSource seam, so tests and the preview can pass a fake.
Try it yourself
- 1
Send fewer, spread out
In the second example, set
redrawEveryto1and send the 500 messages withawaitbetween each. The draw count grows with the messages (495 of the 500 here), because the timer now fires between them. The throttle only helps against bursts. Give the loop a few seconds to finish. - 2
Change the seed
In
createFeed(rows, { every: 1e9, seed: 7 })another seed gives other messages. Applied and late counts change, because late messages are ignored only where something newer exists. The same seed always gives the same counts.
Code & diagrams
The same applyEvent runs in your project. A message older than what a station already has is ignored, a count is clamped to the docks, and nothing throws.
Preview
Console
Nothing logged yet.Expected console output
{"id":"S01","bikes":8,"seq":101} -> applied
{"id":"S01","bikes":12,"seq":99} -> late
{"id":"S01","bikes":8,"seq":101} -> late
{"id":"S02","bikes":30,"seq":102} -> applied
{"id":"S77","bikes":3,"seq":103} -> unknown
not json at all -> bad (SyntaxError)
null -> bad
{"id":"S02","bikes":"4","seq":104} -> bad
Quay: 8 of 20 | Mill: 10 of 10
emptiest first: Quay 40%, Mill 100%
last 5 readings: [ 4, 8, 9, 7, 6 ]500 messages, two draws. applyEvent runs for every message, draw() only when the timer says so. Late messages are ignored only when that station already has something newer.
Preview
Console
Nothing logged yet.Expected console output
stations in the snapshot: 25 | draws so far: 1
after the burst: applied 457 | late ignored 43 | draws 1
after waiting: draws 2
rows drawn: 5
title: 9 stations are below 40% fullThe hidden flag is a stand-in: in a real tab, the browser sets document.hidden and fires visibilitychange itself.
Preview
Console
Nothing logged yet.Expected console output
hidden: messages applied 19 | draws 1
visible again: draws 2
Showing old numbers. Last update 3 minutes ago.
badge after fresh data: null
after stop(): applied unchangedThese need a server of your own, so they are shown and not run. The text an SSE server sends is plain: lines that start with data:, ended by an empty line.
// 1. Polling: ask again after each answer. A timeout chain, not setInterval, so slow answers never pile up.
async function poll() {
try {
const response = await fetch("/api/status");
if (response.ok) apply(await response.json());
} catch { /* offline: keep the old numbers and try again */ }
setTimeout(poll, 5000);
}
// 2. SSE: the browser opens the connection, parses "data:" lines, and reconnects by itself
const source = new EventSource("/api/status/stream");
source.onmessage = (event) => apply(JSON.parse(event.data));
source.onerror = () => { /* the browser retries; show the stale badge meanwhile */ };
// 3. WebSocket: two ways, but no automatic reconnecting: you write it (with backoff)
let delay = 500;
function connect() {
const socket = new WebSocket("wss://example.com/status");
socket.onopen = () => { delay = 500; };
socket.onmessage = (event) => apply(JSON.parse(event.data));
socket.onclose = () => setTimeout(connect, (delay = Math.min(delay * 2, 30000)));
}
connect();Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Draw on every message
In startStatusFeed (the second example), replace schedule(); with draw(); inside source.onmessage.
Myth vs fact
Myth
Real time means a WebSocket.
Fact
A status board that only receives data is fine with polling or SSE. WebSockets add reconnect logic you must write, and some company proxies block them.
Myth
Messages arrive in the order they were sent.
Fact
Not over a network, and not after a reconnect that replays old ones. Use sequence numbers and keep the newest.
Myth
Redraw on every message so the chart is always exact.
Fact
The eye cannot follow more than a few changes a second. Apply every message, draw on a timer.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
SSE supports resuming: the server tags messages with
id:and the browser sendsLast-Event-IDafter a reconnect, so the server can replay what was missed. Polling and WebSocket need that written by hand. - ▸
Over HTTP/1.1 a browser allows only 6 connections per host, and each open SSE stream uses one. Several tabs on a dashboard can starve each other. HTTP/2 multiplexes streams and removes this limit in practice.
- ▸
When a server restarts, every client loses its connection at the same moment and tries to come back at the same moment. Add random jitter to the backoff delay so a thousand screens do not hit the server in the same second.
- ▸
EventSourcereconnects only after a dropped connection. If the server answers with a status other than 200 or with the wrong content type (it must betext/event-stream), the browser gives up for good:readyStatebecomes2(closed) andonerrorfires once. WatchreadyStateand make your own newEventSourceafter a pause. - ▸
A late message can still be the newest for its station, as the second example shows. Keep the sequence number per entity, not globally, unless the server guarantees global order.
Remember this
- 1
The problem this solves: the board wants the dashboard on the wall all day, updating every few seconds. A feed that sends 100 messages a second, in no guaranteed order, would make a naive chart flicker, show old numbers, freeze the tab and leak memory.
- 2
Three ways to receive data. Polling: the page asks every few seconds (
fetchin a loop). It works everywhere, but data is up to one interval old and most answers repeat the last one. Server-Sent Events (SSE): one normal HTTP connection that the server keeps open and writes messages into. It is one way (server to page), the browser reconnects by itself, andEventSourceis the whole API. WebSocket: a two-way connection, text or binary, where you handle reconnecting yourself. A status wall needs one-way data, so polling or SSE is enough. Choose WebSocket when the page must also send often. (A later Hectal Real-time course, not written yet, goes deeper into all three.) - 3
The preview has no server to connect to. SSE and WebSocket need a server of your own, and the preview can only reach this site's data files. So the live examples use
createFeed, an object shaped likeEventSource(onmessage,onerror,close) that sends seeded messages. The code that reads it is the code you keep: in production you pass() => new EventSource(url)instead. The three real client versions are shown below as plain code. - 4
Messages arrive late, twice, and wrong. Networks reorder messages. Give each message a sequence number and keep, per station, the newest one you have seen: an equal or older number is ignored. Also expect messages for stations you do not know, counts above the number of docks, text that is not JSON, and
null. Each gets a count, not a crash. The first example runs all of them. - 5
Apply on arrival, draw on a timer. Updating a Map is cheap, drawing is not. Update the state on every message, mark it dirty, and draw at most once per interval (here 1,000 ms). The eye cannot read 100 changes a second anyway. The second example sends 500 messages in a burst and draws once.
- 6
Rolling windows. A live line chart must not grow forever. Keep only the last N points, or the last 10 minutes, and drop the oldest as new ones arrive. Memory stays fixed and the x scale slides along.
- 7
The tab in the background. Browsers slow timers and pause animation frames in hidden tabs. Keep applying messages (it is cheap and keeps the data right), skip the drawing while
document.hiddenis true, and draw once whenvisibilitychangesays the tab is back. The third example shows this path. - 8
When messages stop, say so, and clean up. If nothing arrives for a while the chart looks fine but is old: show a stale badge (Topic 16.4). And when a box goes away, call
stop(): clear the timers, remove the listener, close the connection. Forgotten timers keep drawing into a page nobody sees (Topic 16.5 counts them).
Explain it without notes
Compare polling, SSE and WebSocket for a dashboard that only displays data.
Why keep a sequence number and ignore older messages?
Why apply messages immediately but draw on a timer?
What should happen while the tab is hidden, and when it comes back?
Practice
Write throttle(fn, ms) that calls fn at most once per ms, using the newest call's arguments. Where would the dashboard use it?
Add a rolling window of the last 60 bikes-docked readings per station to the state. What does the memory cost for 25 stations?
Trade-offs
- ↔
A longer redraw interval saves work and makes numbers steadier, but a shorter one feels more alive. For a status wall, once a second is usually right.
- ↔
Polling is simple but wasteful and slow; SSE and WebSocket are fast but need server support, proxies that allow them, and reconnect handling.
Done when you can
I can say when to choose polling, SSE or WebSocket.
I can ignore late, duplicate, unknown and broken messages without a crash.
I can apply messages at once and draw at most once per interval.
I can keep a rolling window with fixed memory.
I can skip drawing in a hidden tab and draw once when it returns.
I can stop a feed so nothing keeps running.