SSE vs WebSocket vs Long Poll
โฑ๏ธ ~3-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.Five ways to get news โ from calling every 5 minutes to a direct phone line.
Polling
Calling a friend every 5 minutes to ask if they have news.
Simple. Works everywhere. Wastes calls when there's nothing new.
Long Polling
Calling and staying on hold until they have something to say.
Better. Responds the moment news arrives. But you're always on the line.
SSE
Subscribing to a newsletter. They push to you when there's news.
Server pushes only. You can't talk back on the same channel. HTTP/1.1 compatible.
WebSocket
A walkie-talkie. Both sides talk whenever they want.
Full duplex. Low overhead. No auto-reconnect โ you must implement it.
WebRTC
Two phones calling each other directly โ no exchange in between.
Peer-to-peer. Server steps out after handshake. Lowest latency. Complex setup.
Interactive Sandbox
โ Move something, see it react instantly.Protocol
Client makes a new HTTP request every N seconds. Server responds immediately, even if no new data (empty response). Simple but wasteful.
Message timeline
Connection
New TCP connection per poll (unless HTTP/1
Latency
Up to N seconds (poll interval). Average N/2.
Scale
Poor at scale
Best for
Simple status checks
Challenge
Step through all 5 protocols. Understand the connection model before comparing them.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: Why does SSE have a 6-connection limit in HTTP/1.1?
HTTP/1.1 browsers limit concurrent connections to 6 per domain to prevent server overload. SSE connections are persistent, so 6 SSE connections consume all 6 slots. Opening a 7th tab blocks until one closes. HTTP/2 multiplexes all streams over one TCP connection โ the limit disappears. Fix: use HTTP/2, or use a shared SharedWorker with a single SSE connection across all tabs.
Q: What happens when a WebSocket connection drops?
| 1 | // WebSocket does NOT auto-reconnect โ you must implement it |
| 2 | function createWebSocket(url) { |
| 3 | const ws = new WebSocket(url); |
| 4 | let reconnectDelay = 1000; |
| 5 | ย |
| 6 | ws.addEventListener('close', (e) => { |
| 7 | if (!e.wasClean) { // unexpected disconnect |
| 8 | console.log(`Reconnecting in ${reconnectDelay}ms...`); |
| 9 | setTimeout(() => createWebSocket(url), reconnectDelay); |
| 10 | reconnectDelay = Math.min(reconnectDelay * 2, 30000); // backoff |
| 11 | } |
| 12 | }); |
| 13 | ย |
| 14 | ws.addEventListener('open', () => { |
| 15 | reconnectDelay = 1000; // reset on successful connect |
| 16 | }); |
| 17 | ย |
| 18 | return ws; |
| 19 | } |
| 20 | ย |
| 21 | // EventSource (SSE) DOES auto-reconnect: |
| 22 | const es = new EventSource('/events'); |
| 23 | // No reconnect logic needed โ browser handles it |
Q: When is polling actually better than WebSocket?
Polling wins when: (1) update frequency is very low โ checking a job status every 30s doesn't justify a persistent connection; (2) infrastructure can't support persistent connections โ some serverless platforms (Vercel, Lambda) have hard timeouts of 30โ60s; (3) simplicity matters more than efficiency โ polling is debuggable in the Network tab without special tools; (4) CDN caching is valuable โ polling responses can be cached; WebSocket frames cannot.
Bug: no reconnection logic for SSE (but needed for WebSocket)
| 1 | // SSE: EventSource auto-reconnects. Last-Event-ID resumes stream. |
| 2 | const es = new EventSource('/events'); |
| 3 | es.addEventListener('message', (e) => { |
| 4 | console.log('event:', e.data, 'id:', e.lastEventId); |
| 5 | }); |
| 6 | // On reconnect, browser sends: Last-Event-ID: <last id> |
| 7 | // Server resumes from that point |
| 8 | ย |
| 9 | // WebSocket: no auto-reconnect. Missing this = silent data loss. |
| 10 | const ws = new WebSocket(url); |
| 11 | ws.onclose = () => { |
| 12 | // โ BAD: do nothing โ connection lost forever |
| 13 | // โ GOOD: setTimeout(() => reconnect(url), delay) |
| 14 | }; |
The Deep Dive
โ Spec refs, engine internals, the minutiae.WebSocket handshake โ the 101 upgrade
| 1 | // Client sends: |
| 2 | GET /chat HTTP/1.1 |
| 3 | Host: server.example.com |
| 4 | Upgrade: websocket |
| 5 | Connection: Upgrade |
| 6 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== |
| 7 | Sec-WebSocket-Version: 13 |
| 8 | ย |
| 9 | // Server responds: |
| 10 | HTTP/1.1 101 Switching Protocols |
| 11 | Upgrade: websocket |
| 12 | Connection: Upgrade |
| 13 | Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= |
| 14 | // From this point: raw TCP frames, no HTTP overhead |
SSE event format โ Last-Event-ID for resumption
| 1 | // SSE wire format (text/event-stream): |
| 2 | id: 42 |
| 3 | ย |
| 4 | event: message |
| 5 | ย |
| 6 | data: {"text":"hello","user":"alice"} |
| 7 | ย |
| 8 | retry: 3000 |
| 9 | ย |
| 10 | ย |
| 11 | โ double newline ends the event |
| 12 | ย |
| 13 | // Server-side (Node.js / Express): |
| 14 | res.setHeader('Content-Type', 'text/event-stream'); |
| 15 | res.setHeader('Cache-Control', 'no-cache'); |
| 16 | res.setHeader('Connection', 'keep-alive'); |
| 17 | ย |
| 18 | function sendEvent(id, data) { |
| 19 | res.write(`id: ${id} |
| 20 | data: ${JSON.stringify(data)} |
| 21 | ย |
| 22 | `); |
| 23 | } |
| 24 | ย |
| 25 | // Client auto-resumes on reconnect: |
| 26 | req.headers['last-event-id'] // pick up from here |
WebRTC signaling + ICE candidate exchange
| 1 | // Step 1: Client A creates offer (via signaling server) |
| 2 | const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); |
| 3 | const offer = await pc.createOffer(); |
| 4 | await pc.setLocalDescription(offer); |
| 5 | signalingServer.send({ type: 'offer', sdp: offer }); |
| 6 | ย |
| 7 | // Step 2: Client B receives offer, creates answer |
| 8 | const pc = new RTCPeerConnection({ iceServers: [...] }); |
| 9 | await pc.setRemoteDescription(offer); |
| 10 | const answer = await pc.createAnswer(); |
| 11 | await pc.setLocalDescription(answer); |
| 12 | signalingServer.send({ type: 'answer', sdp: answer }); |
| 13 | ย |
| 14 | // Step 3: ICE candidates exchanged until P2P established |
| 15 | pc.onicecandidate = (e) => { |
| 16 | if (e.candidate) signalingServer.send({ type: 'ice', candidate: e.candidate }); |
| 17 | }; |
Comparison table
| Protocol | Direction | Latency | HTTP/1.1 | Auto-reconnect | Server state |
|---|---|---|---|---|---|
| Polling | โโ | N/2 avg | โ | N/A | Stateless |
| Long Polling | โโ | Near 0 | โ | N/A | Holds connection |
| SSE | โ | Near 0 | โ | Auto | Holds stream |
| WebSocket | โ | <10ms | โ | No | Stateful session |
| WebRTC | โท | <50ms | โ | ICE | P2P after signal |
Socket.io โ abstraction layer
Socket.io wraps WebSocket with automatic fallback (long polling), auto-reconnect, room-based broadcasting, and acknowledgements. It adds ~30KB to bundle but eliminates the reconnection boilerplate. Use raw WebSocket for control; Socket.io for rapid development.
Interview Questions
โ Real questions from real interviews โ with answers.WebSocket with a pub/sub broker (Redis); fan out per-document, not per-user.
SSE for server-push only (feeds, notifications); WebSocket when the client also sends data.
Buffer with a bounded queue; drop or throttle when the queue is full; use binary framing to reduce size.
WebSocket connection goes stale on sleep; no auto-reconnect means silent data loss.
Authoritative server holds game state; clients send intents; server broadcasts deltas.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.What is exponential backoff with jitter used for in WebSocket clients?