Chapter 43 — WebSockets and Server-Sent Events
In one sentence
Normal HTTP is the client asking and the server answering; these two let the server push data to the client — one bidirectionally, one simply.
The problem
You want live updates: a chat message, a stock price, a build status, tokens streaming from an AI model. Plain HTTP means the client has to keep asking ("polling"), which is wasteful and laggy.
WebSocket: a two-way pipe
A WebSocket starts as HTTP and then stops being HTTP.
CLIENT: SERVER:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
------------------->
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
<------------------- Sec-WebSocket-Accept: s3pPLM...
=== the SAME TCP connection now carries WebSocket messages, both ways,
until someone closes it ===
Status 101 is the handshake succeeding. After that there are no more HTTP
requests — just messages.
Server-Sent Events: a one-way stream
SSE is far simpler: it's an HTTP response that never ends.
GET /events Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
data: {"price": 10.50}
data: {"price": 10.52}
id: 42
event: alert
data: {"msg": "threshold crossed"}
: this is a comment -- often used as a heartbeat
... connection stays open, more events arrive ...
In the browser it's three lines:
const es = new EventSource('/events');
es.onmessage = e => console.log(JSON.parse(e.data));
Which one to use
| WebSocket | Server-Sent Events | |
|---|---|---|
| Direction | Both ways | Server → client only |
| Protocol | Custom, after upgrade | Plain HTTP |
| Reconnects automatically | No — you write it | Yes, built in |
| Resume after a drop | You build it | Built in (id: + Last-Event-ID) |
| Works through proxies/CDNs | Needs explicit support | Works through anything that streams |
| Binary data | Yes | Text only |
| Browser connection limit | Fine | 6 per origin on HTTP/1.1 — use HTTP/2 |
| Complexity | Higher | Very low |
Rule of thumb: if the client only receives, use SSE. If the client sends frequently too (chat, collaborative editing, games), use WebSocket.
Most "we need WebSockets" requirements are actually satisfied by SSE with far less code. LLM token streaming, notifications, progress bars, and live dashboards are all one-directional.
The hazard that bites everyone: idle timeouts
A long-lived connection with no traffic gets silently killed by something in the middle — a load balancer, a proxy, a NAT (Chapters 12 and 23). Common timeouts: 60 seconds on many load balancers, 350 seconds on AWS NLB, a few minutes on home NAT.
SYMPTOM: the connection dies roughly every N seconds when idle,
with WebSocket close code 1006 ("closed abnormally, no
close frame") and no useful error message.
FIX: send a heartbeat more often than the shortest timeout on the path.
* WebSocket: use protocol ping/pong frames, every 20-30 s
* SSE: send a comment line (":\n\n") every 15-30 s
AND raise the load balancer's idle timeout.
Close code 1006 almost always means a middlebox timeout or a network drop,
not an application error.
Also plan for: reconnect with exponential backoff and jitter (otherwise every client reconnects simultaneously and takes you down), and resume from a cursor so a reconnect doesn't lose or duplicate messages.
Practice (25 min)
A. Watch a WebSocket handshake:
curl -sv -N \
-H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
https://echo.websocket.org/ 2>&1 | grep -iE '^< HTTP|upgrade|sec-websocket'
Look for 101 Switching Protocols.
B. Actually use a WebSocket:
# install: apt install websocat / brew install websocat
websocat wss://echo.websocket.org
# type anything; it echoes back. Ctrl-C to quit.
C. Consume a real SSE stream (Wikipedia publishes every edit, live):
curl -N -H 'Accept: text/event-stream' \
https://stream.wikimedia.org/v2/stream/recentchange | head -20
Note that it never finishes — that's the point. Ctrl-C to stop.
D. In the browser (DevTools console on any page):
const es = new EventSource('https://stream.wikimedia.org/v2/stream/recentchange');
es.onmessage = e => console.log(JSON.parse(e.data).title);
setTimeout(() => es.close(), 10000); // stop after 10 seconds
E. See it in DevTools: Network tab → filter by WS to inspect WebSocket
frames, or click an eventsource request to see the message stream.
Common confusions
- "WebSockets are faster than HTTP." For frequent bidirectional messages, yes — no per-message headers or round trips. For request/response, HTTP/2 is comparable and far simpler.
- "SSE is old / deprecated." No — it's a stable standard, and it's what most AI chat interfaces use for token streaming.
- "Why does my WebSocket keep dying at exactly 60 seconds?" Idle timeout. Add a heartbeat. (Round numbers are always a timer — Chapter 26's lesson again.)
- "Can I use WebSockets through a corporate proxy?" Sometimes. Many block the upgrade. SSE, being ordinary HTTP, usually passes.
Check yourself
- What HTTP status code indicates a successful WebSocket upgrade?
- Name three things SSE gives you for free that you'd have to build with WebSockets.
- What does close code 1006 usually mean?
- Why does a heartbeat fix it?
Further reading
- Docs: MDN, "The WebSocket API" and "Using server-sent events". Both have runnable examples.
- Article: "WebSockets vs Server-Sent Events" — several good comparisons; search that phrase and read one that includes the proxy considerations.
- Video: Hussein Nasser's WebSocket and SSE videos — practical and honest about the operational problems.
- Tool:
websocat— the netcat of WebSockets, invaluable for debugging.