Chats, notifications, live dashboards and collaborative editing all need the server to talk first.
Plain HTTP is client-initiated: the server can only respond. For real-time updates there are three main options.
Polling: ask every few seconds. Simple and cache-friendly, but wasteful and laggy. Long polling (the server holds the request until there's news) reduces lag.
Server-sent events (SSE): the client makes one ordinary GET; the server replies Content-Type: text/event-stream and keeps the response open, writing events as lines of text:
event: price
data: {"symbol":"ACME","value":12.5}followed by a blank line. Browsers use new EventSource(url), which reconnects automatically and resends the last event id in Last-Event-ID. It's one-way (server to client), text-only, and works through normal HTTP infrastructure. Over HTTP/1.1 each stream ties up one of the ~6 connections per origin; HTTP/2 removes that limit. LLM APIs stream tokens this way.
WebSockets: a full-duplex, message-based channel. The client sends a GET with Upgrade: websocket and a Sec-WebSocket-Key; the server answers 101 Switching Protocols, and from then on both sides send frames in either direction, text or binary, with very low overhead. Use wss:// (TLS). You handle reconnection, heartbeats and message formats yourself, and proxies and load balancers must support upgrades and long-lived connections.
Rule of thumb: SSE for server-to-client feeds; WebSockets when the client also sends frequently (chat, games, collaborative editing). The newer WebTransport over HTTP/3 targets similar use cases with multiple streams and unreliable datagrams.
Example
import http from "node:http";
// SSE server: one long-lived response, events as text lines
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
let n = 0;
const timer = setInterval(() => {
n++;
res.write(`id: ${n}\nevent: tick\ndata: ${JSON.stringify({ n })}\n\n`);
if (n === 3) { clearInterval(timer); res.end(); }
}, 100);
});
server.listen(0, async () => {
const res = await fetch(`http://localhost:${server.address().port}/events`);
console.log(await res.text());
// id: 1 / event: tick / data: {"n":1} ... three events, blank-line separated
server.close();
});
// Browser side:
// const es = new EventSource("/events");
// es.addEventListener("tick", (e) => console.log(JSON.parse(e.data)));
// const ws = new WebSocket("wss://chat.example.com"); ws.onmessage = (m) => ...Remember
- SSE = one long GET with text/event-stream; auto-reconnecting, server to client only
- WebSockets upgrade via 101 Switching Protocols to a two-way channel
- Use SSE for feeds, WebSockets for interactive two-way traffic
- Proxies and load balancers must allow long-lived connections
Watch out for
- Using WebSockets for a one-way notification feed and then re-implementing reconnection that SSE gives for free.