WebSockets & Real-Time Communication
📖 Concept
WebSockets provide full-duplex, persistent communication between a client and server over a single TCP connection. Unlike HTTP's request-response model, WebSockets allow either side to send messages at any time — making them essential for real-time applications.
HTTP vs WebSocket
| Feature | HTTP | WebSocket |
|---|---|---|
| Communication | Request → Response (client-initiated) | Bidirectional (either side) |
| Connection | New connection per request (or keep-alive) | Persistent, long-lived |
| Overhead | Headers on every request (~800 bytes) | 2-14 bytes per frame after handshake |
| Use case | REST APIs, page loads | Chat, gaming, live updates |
| Scaling | Stateless, easy to scale | Stateful connections, harder to scale |
The WebSocket Handshake
WebSocket starts as an HTTP request that upgrades to a WebSocket:
- Client sends HTTP request with
Upgrade: websocketheader - Server responds with
101 Switching Protocols - Connection upgrades from HTTP to WebSocket
- Both sides can now send messages freely
Real-Time Alternatives
| Technique | Direction | Latency | Complexity | Best For |
|---|---|---|---|---|
| Polling | Client → Server (repeated) | High (interval-based) | Low | Simple dashboards |
| Long Polling | Client → Server (held open) | Medium | Medium | Notifications |
| SSE (Server-Sent Events) | Server → Client only | Low | Low | Live feeds, stock prices |
| WebSocket | Bidirectional | Very Low | High | Chat, gaming, collaboration |
Scaling WebSocket Connections
The biggest challenge with WebSockets is scaling. Since connections are persistent and stateful:
- Each server can hold ~50K-500K concurrent WebSocket connections (limited by memory and file descriptors)
- You need a pub/sub layer (Redis, Kafka) so that if User A is connected to Server 1 and User B is connected to Server 2, messages between them are routed correctly
- Sticky sessions or a connection registry is needed so clients reconnect to the right server
- Connection draining is needed during deployments — gracefully close old connections and let clients reconnect
Pro tip: If you only need server-to-client updates (one-way), use Server-Sent Events (SSE) instead of WebSockets — they're simpler, work over standard HTTP, and auto-reconnect.
💻 Code Example
1// ============================================2// WebSocket & Real-Time Communication Patterns3// ============================================45// ---------- Pattern 1: Simple WebSocket Server ----------67const WebSocket = require('ws');8const http = require('http');910const server = http.createServer();11const wss = new WebSocket.Server({ server });1213// Track all connected clients14const clients = new Map(); // userId → WebSocket connection1516wss.on('connection', (ws, req) => {17 const userId = req.url.split('?userId=')[1];18 clients.set(userId, ws);19 console.log(`User \${userId} connected. Total: \${clients.size}`);2021 ws.on('message', (data) => {22 const message = JSON.parse(data);23 handleMessage(userId, message);24 });2526 ws.on('close', () => {27 clients.delete(userId);28 console.log(`User \${userId} disconnected. Total: \${clients.size}`);29 });3031 // Send heartbeat every 30 seconds to detect dead connections32 const heartbeat = setInterval(() => {33 if (ws.readyState === WebSocket.OPEN) {34 ws.ping();35 } else {36 clearInterval(heartbeat);37 }38 }, 30000);39});4041function handleMessage(senderId, message) {42 switch (message.type) {43 case 'direct_message':44 // Send to specific user45 const recipientWs = clients.get(message.recipientId);46 if (recipientWs && recipientWs.readyState === WebSocket.OPEN) {47 recipientWs.send(JSON.stringify({48 type: 'new_message',49 from: senderId,50 content: message.content,51 timestamp: Date.now(),52 }));53 }54 break;5556 case 'broadcast':57 // Send to all connected users58 clients.forEach((ws, id) => {59 if (id !== senderId && ws.readyState === WebSocket.OPEN) {60 ws.send(JSON.stringify({61 type: 'broadcast',62 from: senderId,63 content: message.content,64 }));65 }66 });67 break;68 }69}7071// ---------- Pattern 2: Scaling with Pub/Sub (Redis) ----------7273// ❌ BAD: Single-server WebSocket — doesn't scale74// When you have 2+ servers behind a load balancer,75// User A on Server 1 can't send to User B on Server 27677// ✅ GOOD: Redis Pub/Sub for cross-server messaging78const Redis = require('ioredis');79const pub = new Redis(); // Publisher80const sub = new Redis(); // Subscriber8182// Each server subscribes to a channel83sub.subscribe('chat-messages');8485sub.on('message', (channel, data) => {86 const message = JSON.parse(data);87 // Deliver to local clients connected to THIS server88 const recipientWs = clients.get(message.recipientId);89 if (recipientWs && recipientWs.readyState === WebSocket.OPEN) {90 recipientWs.send(JSON.stringify(message));91 }92});9394// When a user sends a message, publish to Redis (all servers receive it)95function handleMessageScalable(senderId, message) {96 pub.publish('chat-messages', JSON.stringify({97 type: 'new_message',98 from: senderId,99 recipientId: message.recipientId,100 content: message.content,101 timestamp: Date.now(),102 sourceServer: process.env.SERVER_ID, // Track which server sent it103 }));104}105106// ---------- Pattern 3: Server-Sent Events (SSE) — Simpler Alternative ----------107108// SSE is one-way (server → client), uses standard HTTP, auto-reconnects109const express = require('express');110const sseApp = express();111112sseApp.get('/api/events', (req, res) => {113 // Set SSE headers114 res.set({115 'Content-Type': 'text/event-stream',116 'Cache-Control': 'no-cache',117 'Connection': 'keep-alive',118 });119120 // Send a comment to prevent proxy timeout121 res.write(':keep-alive\n\n');122123 // Send events as they happen124 const sendEvent = (eventType, data) => {125 res.write(`event: \${eventType}\n`);126 res.write(`data: \${JSON.stringify(data)}\n\n`);127 };128129 // Example: Send stock price updates every second130 const interval = setInterval(() => {131 sendEvent('price-update', {132 symbol: 'AAPL',133 price: (150 + Math.random() * 10).toFixed(2),134 timestamp: Date.now(),135 });136 }, 1000);137138 // Cleanup on disconnect139 req.on('close', () => {140 clearInterval(interval);141 console.log('SSE client disconnected');142 });143});144145// Client-side SSE (in browser):146// const eventSource = new EventSource('/api/events');147// eventSource.addEventListener('price-update', (event) => {148// const data = JSON.parse(event.data);149// updateUI(data);150// });151152server.listen(8080, () => console.log('WebSocket server on port 8080'));
🏋️ Practice Exercise
Choose the Right Tool: For each scenario, decide between polling, long polling, SSE, or WebSocket. Justify your choice: (a) Stock ticker dashboard, (b) Multiplayer game, (c) Email inbox notifications, (d) Collaborative document editing, (e) Social media live comment stream.
Scale WebSockets: Design a system where 1 million users can simultaneously participate in a live chat during a sports event. How many WebSocket servers do you need? How do messages route between servers?
Heartbeat Design: Implement a WebSocket heartbeat mechanism where: (a) server pings every 30s, (b) client must respond within 5s, (c) 3 missed pongs = connection considered dead, (d) client auto-reconnects with exponential backoff.
SSE vs WebSocket Trade-off: You're building a live sports score app. Write a pros/cons comparison for SSE vs WebSocket. Which would you choose and why?
Connection Recovery: Design a reconnection strategy for a chat application where the user loses WiFi for 30 seconds. How do you ensure no messages are lost? What about message ordering?
⚠️ Common Mistakes
Using WebSockets when SSE or polling would suffice — WebSockets add significant complexity (connection management, scaling, heartbeats). If you only need server → client updates, use SSE. If updates are infrequent, use polling.
Not implementing heartbeats — without heartbeats, half-open connections (where the client has disconnected but the server doesn't know) accumulate and waste server resources. Always ping/pong.
Forgetting about reconnection logic — networks are unreliable. Clients MUST implement auto-reconnect with exponential backoff. Without it, a brief network blip permanently disconnects users.
Trying to scale WebSockets without a pub/sub layer — with multiple servers behind a load balancer, messages between users on different servers will be lost unless you add Redis Pub/Sub or a similar message routing layer.
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for WebSockets & Real-Time Communication