Frontend Labs
⋯

62. WebSockets and Server-Sent Events

62.1 WebSocket: the upgrade

A WebSocket starts life as an HTTP request and then stops being HTTP.

CLIENT:
  GET /chat HTTP/1.1
  Host: example.com
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
  Sec-WebSocket-Version: 13
  Sec-WebSocket-Protocol: chat            (optional subprotocol negotiation)
  Origin: https://example.com

SERVER:
  HTTP/1.1 101 Switching Protocols
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=      (SHA-1 of key + magic GUID)

-> the same TCP connection now carries WebSocket FRAMES, bidirectionally,
   until either side sends a Close frame.

The Sec-WebSocket-Accept handshake exists to prove the server understood the upgrade (defeating cache-poisoning attacks against naive proxies), not for security.

62.2 Frames and operational details

Aspect Detail
Framing Opcodes: text(1), binary(2), close(8), ping(9), pong(10). Continuation frames for fragmentation.
Masking Client-to-server frames must be XOR-masked with a random key (an anti-cache-poisoning measure). Server-to-client must not be.
Ping/pong The protocol's keepalive. Use it — see below.
Close A close frame carries a status code (1000 normal, 1001 going away, 1006 = abnormal/no close frame seen)
Subprotocols Sec-WebSocket-Protocol negotiates an application protocol (e.g. graphql-ws)
Compression permessage-deflate extension; watch CPU and CRIME-style risks with mixed secret/attacker data
Over HTTP/2 RFC 8441 :protocol extended CONNECT; over HTTP/3 similarly. Many stacks still downgrade to HTTP/1.1 for WS.

62.3 The operational hazards

  • Idle timeouts kill you. Every proxy, load balancer, and NAT has one (commonly 60 s, AWS ALB default 60 s, NLB 350 s). A WebSocket with no traffic is silently reaped. Send a ping every 20–30 s, from the server, and expect pongs.
  • 1006 means "connection died without a close frame" — almost always a middlebox timeout, a network drop, or a crashed process. It carries no reason, so log your own context.
  • Load balancers must be configured for it. L7 LBs need WebSocket support enabled; sticky routing matters because the connection is long-lived and stateful.
  • Scaling is stateful. N servers × M connections; broadcasting requires a pub/sub bus (Redis, NATS) because a message must reach whichever server holds each connection.
  • Reconnect with backoff and jitter, and resume with a cursor — a mass-disconnect otherwise produces a thundering herd.

62.4 Server-Sent Events: the simpler option

SSE is plain HTTP with a never-ending response body.

Request:   GET /events    Accept: text/event-stream

Response:  HTTP/1.1 200 OK
           Content-Type: text/event-stream
           Cache-Control: no-cache
           Connection: keep-alive

           event: price
           id: 42
           data: {"symbol":"ACME","px":10.5}

           : this is a comment, used as a heartbeat

           data: another message
WebSocket SSE
Direction Bidirectional Server → client only
Protocol Custom framing after upgrade Plain HTTP
Reconnect You implement it Automatic, built into EventSource
Event IDs / resume You implement it id: + Last-Event-ID header, built in
Proxy/CDN friendliness Needs explicit support Works through anything that streams
Binary Yes Text only (base64 if you must)
Browser connection limit Fine 6 per origin on HTTP/1.1 (use HTTP/2)

Choose SSE for one-way feeds (notifications, dashboards, LLM token streaming, progress). Choose WebSocket when the client must push frequently (chat, collaborative editing, games).

Also consider long polling as a fallback, and note that plain HTTP chunked streaming (what fetch + ReadableStream consumes) is often enough.

62.5 Real-world example

A trading dashboard used WebSockets behind an ALB with the default 60-second idle timeout and no application ping. During quiet markets, connections dropped every minute with code 1006, the client reconnected, and the reconnect storm at the open took down the service. Fix: server-side ping every 25 s, ALB idle timeout raised to 300 s, exponential backoff with jitter on reconnect, and Last-Event-ID- style resume. Disconnects fell to near zero.

Try it yourself

# watch the upgrade 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'

# consume an SSE stream (note it never ends)
curl -N -H 'Accept: text/event-stream' https://stream.wikimedia.org/v2/stream/recentchange | head -20

# websocat is the netcat of websockets
websocat wss://echo.websocket.org

Generated from real-life-example.md · 8 Oct 2026 16:36