WebSocket & Server-Sent Events
Learn long polling, WebSocket, Server-Sent Events, and how to choose a real-time transport without opening external network connections.
- 01Explain the transport choicesCompare short polling, long polling, SSE, and WebSocket with everyday analogies.
- 02Use the browser APIs safelyName WebSocket ready states, close codes, EventSource reconnection, and SSE text fields.
- 03Choose the right toolPick a transport for chat, scores, notifications, games, and slow job status checks.
Real-time means “tell me when it happens”
Most network code you have seen so far is request/response: your code asks a server for something, the server replies, and the connection is done. That is perfect for loading a profile, saving a form, or fetching JSON. Real-time user interfaces have a different feeling: scores change while you watch, a chat bubble appears without refreshing, and a dashboard moves as data arrives.
JavaScript gives you several ways to create that feeling. They are not interchangeable magic words. They differ in direction, reconnection behavior, binary support, how well they fit proxies, and how much work your app must do. This lesson covers the four names you will hear most often: short polling, long polling, WebSocket, and Server-Sent Events.
The question is not “which one is newest?” It is “who needs to talk, how often, and what kind of data?” A score ticker mostly listens. A multiplayer game talks both ways. A report status page might only need to check once a minute.
- In real life: Refresh the score every minute
- In JavaScript: A timer calls
fetch()again and again - In real life: Ask once and wait for the next score
- In JavaScript: One request waits until an update or timeout, then you ask again
- In real life: The score page sends every update
- In JavaScript: The server streams text events to the browser
- In real life: You can send a message about the match
- In JavaScript: Client and server send messages any time over one open connection
Where the analogy stops: Networks fail, servers restart, tabs sleep, and proxies buffer data. The analogy explains direction and timing, not reliability guarantees.
This site is static and never connects to external hosts. The WebSocket and long-polling demos use an in-page fake server that follows the real API shapes. The SSE demo first tries a browser-only data: event stream; if the browser blocks it, it falls back to a tested parser.
Long polling: request, wait, answer, repeat
INTERACTIVEShort polling is the simplest pattern: set a timer and ask the server, “anything new?” every few seconds. It works almost everywhere because it is just normal HTTP. The cost is wasted work: quiet periods still create empty requests, and updates can wait until the next timer tick.
Long polling changes one detail. The browser sends a request, but the server does not answer immediately. It holds the request until there is a new event or a timeout. As soon as the response arrives, the browser processes it and immediately starts the next request. You still pay one request per message, but you avoid a stream of empty “no change” replies.
// Short polling: ask every 10 secondsGET /updates?since=0 -> no changeGET /updates?since=0 -> update at 20s // Long polling: ask once and waitGET /updates?since=0 -> server holds update at 18sShort polling makes 6 requests per minute and can wait 2s after the update. Long polling keeps one request waiting, so the update is returned immediately in this model.
Long polling is useful when WebSocket or streaming connections are blocked by old infrastructure, or when the server stack is already built around ordinary request handlers. It is also a good fallback mental model: a client loop, a timeout, and careful error handling. You saw similar retry thinking in the Async patterns lesson.
WebSocket: one open, two-way conversation
STEP THROUGHA WebSocket starts with new WebSocket("wss://…", protocols?). After the opening handshake, the browser and server can both send messages at any time. That makes it the usual choice for chat, multiplayer games, collaborative cursors, and other interfaces where the client also speaks often.
Calling someone does not mean you can talk instantly. First the phone dials, then the call connects. WebSocket has the same safety rule: send only after the open event, when readyState is OPEN.
- In real life: Dialing
- In JavaScript:
CONNECTING/ readyState0 - In real life: Connected call
- In JavaScript:
OPEN/ readyState1;send()is allowed - In real life: Hanging up
- In JavaScript:
CLOSING/ readyState2 - In real life: Hung up
- In JavaScript:
CLOSED/ readyState3 - In real life: Saying ‘still there?’ on a quiet call
- In JavaScript: Heartbeat ping/pong messages
Where the analogy stops: The browser WebSocket API does not expose protocol-level ping frames to page JavaScript. Apps often send their own small heartbeat messages instead.
This is an instrumented replay using a simulated WebSocket. It follows the real WebSocket event names and readyState values without opening any network connection.
script
socket.addEventListener("open", () => { socket.send("hello bots");}); socket.addEventListener("message", (event) => { console.log(event.data);}); socket.addEventListener("close", (event) => { console.log(event.code, event.reason);});A few details matter in production. send() accepts strings, Blob, ArrayBuffer, and typed-array data only when the socket is open; sending while connecting throws InvalidStateError. bufferedAmount shows bytes queued but not yet sent. Close code 1000 means normal, 1001 means going away, and 1006 is special: apps cannot send it, but browsers report it when the connection disappears abnormally. WebSocket does not reconnect automatically. If the page was loaded over HTTPS, use wss://.
const socket = new WebSocket("wss://example.test/chat", "chat.v1"); socket.addEventListener("open", () => { socket.send("Hello room!");}); socket.addEventListener("message", (event) => { appendToLog(event.data);}); socket.addEventListener("close", (event) => { showCloseCode(event.code, event.reason);});- Disconnected. Press Connect to start the fake room.
This is a simulation: no network leaves the page. It uses the real WebSocket event names and readyState numbers. Current state: CLOSED.
The playground’s drop button reports 1006 to make the abnormal case visible. The reconnect button uses a tiny backoff: wait a bit, create a new socket, then wait longer on the next failure. Real apps add maximum delays, online/offline checks, and authentication refreshes.
WebSocketStream is a newer streams-based idea available only in Chromium-based browsers at the time of writing. The widely used API is still WebSocket.
Server-Sent Events: a one-way text stream
INTERACTIVEServer-Sent Events use EventSource to receive a text/event-stream response over HTTP. The direction is one-way: server to browser. That is a strength when your app mostly listens, because the browser gives you automatic reconnection, named events, event ids, and retry timing without writing a custom socket manager.
The stream is plain text. Lines beginning with event: name the event for addEventListener(name). Lines beginning with data: become event.data. id: sets lastEventId; on reconnect, browsers send it back as the Last-Event-ID header when using normal HTTP. retry: suggests how long to wait before reconnecting. Unnamed events go to onmessage; named events do not.
const source = new EventSource("data:text/event-stream,..."); source.addEventListener("score", (event) => { console.log(event.lastEventId, event.data);}); source.onmessage = (event) => { console.log("message", event.data);}; source.close(); // stop reconnectingretry: 1200id: kickoffevent: scoredata: Owls 0, Foxes 0 id: goal-1event: scoredata: Owls 1, Foxes 0 id: note-1data: Halftime snacks are ready- Press a button to receive or parse events.
Not started Mode: parser. The fallback parser is a pure function tested in Node.
SSE is text-only, but text can still contain JSON. It is great for notification feeds, live sports scores, progress messages, logs, and dashboards. Use new EventSource(url, { withCredentials: true }) when cookies or HTTP authentication should be included. In many browsers, several SSE tabs on the same HTTP/1.1 domain can run into the per-domain connection limit; HTTP/2 usually improves that because streams share one connection.
Choosing a transport
SORT IT| Feature | Polling / long polling | Server-Sent Events | WebSocket |
|---|---|---|---|
| Direction | Client asks; server answers | Server → client | Both directions any time |
| Protocol shape | Normal HTTP requests | HTTP response with text/event-stream | WebSocket connection after an HTTP upgrade |
| Reconnection | Your loop decides | Built in with retry and Last-Event-ID | Your app decides |
| Binary support | Yes, as normal responses | No, UTF-8 text | Yes, binary frames and text |
| HTTP/proxy friendliness | Excellent | Good, though buffering and connection limits matter | Sometimes needs proxy support for upgrades |
| Typical uses | Slow job status, legacy fallback | Scores, notifications, progress, dashboards | Chat, games, collaboration, live control |
- Live sports scores: server pushes updates, browser mostly listens
- Multiplayer game: every player sends moves and receives state quickly
- Check whether a report finished once every minute
- Chat room: people type any time and receive messages any time
- Notifications feed: new items appear while the tab is open
- Very old infrastructure where streaming is blocked by proxies
Sort each scenario by the simplest transport that fits its direction and timing needs.
A practical rule of thumb: choose the boring thing that satisfies the product. If rare updates are fine, poll. If the server streams text and the browser only listens, start with SSE. If both sides need to talk at any moment, use WebSocket. If infrastructure says no, long polling is still a respectable fallback.
Where you will use this
In a real app, the code around the transport matters as much as the transport: parse messages, update state, show connection health, clean up when a component unmounts, and avoid duplicate listeners. These examples omit framework state on purpose so the transport shape is visible.
const socket = new WebSocket("wss://example.com/chat", "chat.v1"); socket.addEventListener("open", () => { socket.send(JSON.stringify({ type: "join", room: "lobby" }));}); socket.addEventListener("message", (event) => { const message = JSON.parse(event.data); renderChatMessage(message);}); socket.addEventListener("close", (event) => { scheduleReconnect(event.code);});const feed = new EventSource("/notifications/stream", { withCredentials: true,}); feed.addEventListener("notification", (event) => { const item = JSON.parse(event.data); addNotification(item);}); feed.onerror = () => { showStatus("Reconnecting...");};function chooseRealtimeTransport({ clientTalks, binary, textUpdates, rareUpdates }) { if (clientTalks || binary) return "WebSocket"; if (textUpdates) return "Server-Sent Events"; if (rareUpdates) return "Polling"; return "Long polling";}The cleanup step is easy to forget. Components should call socket.close() or eventSource.close() when they no longer need updates. Otherwise, a hidden tab or unmounted view can keep receiving data and doing work.
Common misconceptions
- “Real-time always means WebSocket.” If data only flows from server to browser, SSE is often simpler and reconnects for you.
- “SSE can replace every WebSocket.” SSE is one-way and text only. Use another request for client actions, or choose WebSocket.
- “WebSocket reconnects automatically.” It does not. A close event is the end until your code creates a new socket.
- “Close code 1006 is something I should send.” It is reserved for reporting abnormal closure. Application code should not send it.
- “Long polling is fake real-time.” It is a real and useful pattern, especially as a compatibility fallback, but it pays request overhead per message.
- “A heartbeat fixes every network problem.” Heartbeats detect quiet broken connections; they do not guarantee delivery or replace retries.
Practice exercises
4 EXERCISESRead the code and predict the one line printed to the console.
class TinySocket {
constructor() { this.readyState = 0; }
send() {
if (this.readyState === 0) throw new Error("InvalidStateError");
console.log("sent");
}
}
const socket = new TinySocket();
try {
socket.send("hi");
} catch (error) {
console.log(error.message);
}The program catches the thrown error and logs its message: InvalidStateError. Real WebSocket behaves the same way when send() happens while CONNECTING.
Type the readyState number that means a WebSocket is open.
OPEN is readyState 1. CONNECTING is 0, CLOSING is 2, and CLOSED is 3.
Predict how many data fields the small parser counts.
const lines = ["event: score", "data: Owls 1", "", "data: plain", ""];
console.log(lines.filter((line) => line.startsWith("data:")).length);The snippet filters two data: lines: data: Owls 1 and data: plain. Event names and blank lines do not count.
Use the clues in the booleans to choose the transport the program prints.
const needsClientMessages = true;
const binaryFrames = true;
console.log(needsClientMessages && binaryFrames ? "WebSocket" : "SSE");const needsClientMessages = true;
const binaryFrames = true;
console.log(needsClientMessages && binaryFrames ? "WebSocket" : "SSE");The snippet prints WebSocket. Two-way client messages and binary frames are both WebSocket strengths; SSE is one-way text.
Check your understanding
7 QUESTIONSQuestion 1 of 7Which comparison best describes long polling?
Choose an answer to see the explanation.
Question 2 of 7What does this readyState lookup print?
Read the code, then predictconst states = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"]; console.log(states[1]);Choose an answer to see the explanation.
Question 3 of 7Which WebSocket statement is accurate?
Choose an answer to see the explanation.
Question 4 of 7Does this SSE text contain a named event?
Read the code, then predictconst stream = "event: score\ndata: 2-1\n\ndata: plain\n\n"; console.log(stream.includes("event: score"));Choose an answer to see the explanation.
Question 5 of 7Which EventSource feature is built in?
Choose an answer to see the explanation.
Question 6 of 7Which transport would you choose for multiplayer movement updates?
Choose an answer to see the explanation.
Question 7 of 7What does this transport check print?
Read the code, then predictconst supportsBinary = { sse: false, websocket: true }; console.log(supportsBinary.websocket ? "binary ok" : "text only");Choose an answer to see the explanation.
Key takeaways
- Polling asks on a schedule; long polling waits to answer one request.
- WebSocket is a two-way open connection. Send only in
OPEN, and reconnect yourself. - Server-Sent Events are one-way text streams with named events, ids, retry, and automatic reconnection.
- Choose by direction, update frequency, binary needs, infrastructure, and failure behavior.
Real-time browser code is choosing the smallest reliable channel that delivers updates when users need them.
Up next: XMLHttpRequest — the older request API you’ll still meet in legacy code.