Topic Overview
WebSockets
Learn WebSocket protocol for full-duplex, persistent communication between client and server. Understand handshake, frame format, and use cases.
What are WebSockets?
WebSocket provides:
- Full-duplex communication: Both client and server can send data simultaneously
- Persistent connection: Connection stays open (unlike HTTP)
- Low overhead: Minimal protocol overhead after handshake
- Real-time: Instant bidirectional communication
Use cases:
- Real-time chat applications
- Live notifications
- Collaborative editing
- Gaming
- Financial tickers
- Live sports scores
WebSocket vs HTTP
HTTP (Request-Response)
Client → Server: GET /api/data
Server → Client: Response (connection closes)
For updates: Client must poll repeatedly
Client → Server: GET /api/data (poll 1)
Client → Server: GET /api/data (poll 2)
Client → Server: GET /api/data (poll 3)
Problems:
- Polling overhead: Repeated requests
- Latency: Delay between polls
- Server load: Many unnecessary requests
WebSocket (Persistent Connection)
Client ↔ Server: Persistent connection
Client → Server: Message 1
Server → Client: Message 2
Client → Server: Message 3
Server → Client: Message 4
(Connection stays open)
Benefits:
- No polling: Server can push data immediately
- Low latency: Instant communication
- Efficient: Single connection, minimal overhead
WebSocket Handshake
WebSocket starts as HTTP request, then upgrades to WebSocket protocol.
Client Request
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Key headers:
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Key: Random base64-encoded key
- Sec-WebSocket-Version: Protocol version (13)
Server Response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Key points:
- Status 101: Switching Protocols
- Sec-WebSocket-Accept: Computed from client key + magic string
Key Calculation
import hashlib
import base64
def compute_accept_key(client_key):
"""Compute Sec-WebSocket-Accept from client key"""
magic_string = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
combined = client_key + magic_string
sha1_hash = hashlib.sha1(combined.encode()).digest()
return base64.b64encode(sha1_hash).decode()
# Example
client_key = "dGhlIHNhbXBsZSBub25jZQ=="
accept_key = compute_accept_key(client_key)
# Result: "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
WebSocket Frame Format
After handshake, data sent in frames:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
Fields:
- FIN: Final fragment (1) or more fragments (0)
- RSV: Reserved bits
- Opcode: Frame type (text, binary, close, ping, pong)
- MASK: Masking key present (client must mask, server must not)
- Payload length: Length of payload data
- Masking key: 32-bit key (if MASK=1)
- Payload data: Actual data
Examples
WebSocket Server (Node.js)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
console.log('Client connected');
// Send welcome message
ws.send('Welcome to WebSocket server!');
// Handle messages
ws.on('message', (message) => {
console.log(`Received: ${message}`);
// Echo back
ws.send(`Echo: ${message}`);
});
// Handle close
ws.on('close', () => {
console.log('Client disconnected');
});
// Handle errors
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
});
WebSocket Client (JavaScript)
// Create WebSocket connection
const ws = new WebSocket('ws://localhost:8080');
// Connection opened
ws.onopen = () => {
console.log('Connected to server');
ws.send('Hello Server!');
};
// Receive message
ws.onmessage = (event) => {
console.log('Received:', event.data);
};
// Connection closed
ws.onclose = () => {
console.log('Connection closed');
};
// Error
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Send message
function sendMessage(message) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
}
WebSocket with Authentication
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const wss = new WebSocket.Server({
port: 8080,
verifyClient: (info) => {
// Verify token from query string or headers
const token = new URL(info.req.url, 'http://localhost').searchParams.get('token');
try {
jwt.verify(token, 'secret');
return true;
} catch {
return false;
}
}
});
wss.on('connection', (ws, req) => {
// Extract user from token
const token = new URL(req.url, 'http://localhost').searchParams.get('token');
const user = jwt.decode(token);
console.log(`User ${user.id} connected`);
ws.on('message', (message) => {
// Broadcast to all clients
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(`${user.id}: ${message}`);
}
});
});
});
WebSocket Reconnection
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectInterval = 1000;
this.maxReconnectInterval = 30000;
this.reconnectDecay = 1.5;
this.shouldReconnect = true;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectInterval = 1000; // Reset on successful connection
};
this.ws.onclose = () => {
if (this.shouldReconnect) {
this.reconnect();
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
reconnect() {
setTimeout(() => {
console.log('Reconnecting...');
this.connect();
this.reconnectInterval = Math.min(
this.reconnectInterval * this.reconnectDecay,
this.maxReconnectInterval
);
}, this.reconnectInterval);
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
}
}
close() {
this.shouldReconnect = false;
if (this.ws) {
this.ws.close();
}
}
}
// Usage
const client = new WebSocketClient('ws://localhost:8080');
client.connect();
Common Pitfalls
- Not handling reconnection: Connection drops, client doesn't reconnect. Fix: Implement automatic reconnection with exponential backoff
- No heartbeat/ping: Don't know if connection is alive. Fix: Send ping frames periodically, close if no pong
- Not handling backpressure: Sending too fast, overwhelming server. Fix: Check
bufferedAmount, throttle sending - Security issues: Not validating origin, no authentication. Fix: Validate origin, implement authentication
- Memory leaks: Not cleaning up event listeners. Fix: Remove listeners on close, limit connection lifetime
- Not handling frame fragmentation: Large messages split into multiple frames. Fix: Handle FIN bit, reassemble fragments
Interview Questions
Beginner
Q: What are WebSockets and why are they used instead of HTTP?
A:
WebSockets provide full-duplex, persistent connections between client and server.
Why used instead of HTTP:
- Real-time communication: Server can push data immediately (no polling)
- Lower latency: No HTTP overhead after handshake
- Efficient: Single persistent connection vs multiple HTTP requests
- Bidirectional: Both client and server can send data simultaneously
HTTP vs WebSocket:
HTTP (Request-Response):
Client → Server: GET /api/data
Server → Client: Response (connection closes)
For updates: Must poll repeatedly
WebSocket (Persistent):
Client ↔ Server: Persistent connection
Server → Client: Push data immediately
No polling needed
Use cases:
- Real-time chat
- Live notifications
- Collaborative editing
- Gaming
- Financial tickers
Intermediate
Q: Explain the WebSocket handshake process and frame format.
A:
Handshake Process:
-
Client Request (HTTP Upgrade)
GET /chat HTTP/1.1 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 -
Server Response (101 Switching Protocols)
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= -
Key Calculation
accept_key = base64(sha1(client_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
Frame Format:
[FIN|RSV|Opcode|MASK|Payload Length|Masking Key|Payload Data]
Fields:
- FIN: Final fragment (1) or more fragments (0)
- Opcode: Frame type (0x1=text, 0x2=binary, 0x8=close, 0x9=ping, 0xA=pong)
- MASK: Masking key present (client must mask, server must not)
- Payload length: Length of data (7, 16, or 64 bits)
- Masking key: 32-bit key (if MASK=1, client frames)
- Payload data: Actual message data
After handshake:
- Connection upgraded to WebSocket protocol
- Data sent in frames (not HTTP)
- Connection stays open until closed
Senior
Q: Design a scalable WebSocket system that handles millions of concurrent connections. How do you handle load balancing, message routing, and connection management?
A:
class ScalableWebSocketSystem {
private servers: WebSocketServer[];
private loadBalancer: LoadBalancer;
private messageRouter: MessageRouter;
private connectionManager: ConnectionManager;
private pubsub: PubSub;
constructor() {
// Multiple WebSocket servers
this.servers = [
new WebSocketServer({ port: 8080 }),
new WebSocketServer({ port: 8081 }),
new WebSocketServer({ port: 8082 })
];
this.loadBalancer = new LoadBalancer();
this.messageRouter = new MessageRouter();
this.connectionManager = new ConnectionManager();
this.pubsub = new PubSub(); // Redis Pub/Sub
}
// 1. Load Balancing
class LoadBalancer {
async routeConnection(request: Request): Promise<WebSocketServer> {
// Sticky sessions: Route same client to same server
const clientId = this.getClientId(request);
const server = this.getServerForClient(clientId);
// Or: Least connections
const server = this.servers.reduce((min, s) =>
s.connectionCount < min.connectionCount ? s : min
);
return server;
}
}
// 2. Connection Management
class ConnectionManager {
private connections: Map<string, Connection>; // user_id → connection
async handleConnection(ws: WebSocket, userId: string): Promise<void> {
// Store connection
this.connections.set(userId, {
ws,
serverId: this.getServerId(),
connectedAt: Date.now()
});
// Subscribe to user's channels
await this.pubsub.subscribe(`user:${userId}`, (message) => {
this.sendToUser(userId, message);
});
// Handle disconnection
ws.on('close', () => {
this.connections.delete(userId);
this.pubsub.unsubscribe(`user:${userId}`);
});
}
async sendToUser(userId: string, message: any): Promise<void> {
const connection = this.connections.get(userId);
if (connection && connection.ws.readyState === WebSocket.OPEN) {
connection.ws.send(JSON.stringify(message));
}
}
}
// 3. Message Routing (Cross-Server)
class MessageRouter {
async routeMessage(fromUserId: string, toUserId: string, message: any): Promise<void> {
// Check if user connected to this server
const connection = this.connectionManager.getConnection(toUserId);
if (connection && connection.serverId === this.getServerId()) {
// Local: Send directly
await this.connectionManager.sendToUser(toUserId, message);
} else {
// Remote: Publish to pub/sub
await this.pubsub.publish(`user:${toUserId}`, message);
}
}
}
// 4. Pub/Sub for Cross-Server Communication
class PubSub {
async publish(channel: string, message: any): Promise<void> {
// Publish to Redis
await this.redis.publish(channel, JSON.stringify(message));
}
async subscribe(channel: string, callback: Function): Promise<void> {
// Subscribe to Redis channel
this.redis.subscribe(channel);
this.redis.on('message', (ch, msg) => {
if (ch === channel) {
callback(JSON.parse(msg));
}
});
}
}
// 5. Connection Pooling
class ConnectionPool {
private pools: Map<string, WebSocket[]>; // server → connections
async getConnection(server: string): Promise<WebSocket> {
let pool = this.pools.get(server);
if (!pool) {
pool = [];
this.pools.set(server, pool);
}
// Reuse existing connection
const connection = pool.find(ws => ws.readyState === WebSocket.OPEN);
if (connection) {
return connection;
}
// Create new connection
const ws = new WebSocket(`ws://${server}`);
pool.push(ws);
return ws;
}
}
// 6. Heartbeat/Ping
class HeartbeatManager {
startHeartbeat(ws: WebSocket): void {
const interval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
} else {
clearInterval(interval);
}
}, 30000); // 30 seconds
ws.on('pong', () => {
// Connection alive
});
ws.on('close', () => {
clearInterval(interval);
});
}
}
// 7. Rate Limiting
class RateLimiter {
async checkRateLimit(userId: string): Promise<boolean> {
const key = `ratelimit:${userId}`;
const count = await this.redis.incr(key);
if (count === 1) {
await this.redis.expire(key, 60); // 1 minute window
}
return count <= 100; // 100 messages per minute
}
}
}
Features:
- Load balancing: Distribute connections across servers
- Message routing: Route messages to correct server
- Pub/Sub: Cross-server communication via Redis
- Connection management: Track connections, handle disconnections
- Heartbeat: Keep connections alive, detect dead connections
- Rate limiting: Prevent abuse
- Scaling: Horizontal scaling with multiple servers
Key Takeaways
- WebSocket: Full-duplex, persistent connection for real-time communication
- Handshake: Starts as HTTP, upgrades to WebSocket (101 Switching Protocols)
- Frame format: Binary frames with opcode, mask, payload length, data
- Benefits: No polling, low latency, efficient, bidirectional
- Use cases: Real-time chat, notifications, gaming, collaborative editing
- Connection management: Handle reconnection, heartbeat, backpressure
- Scaling: Load balancing, message routing, pub/sub for cross-server communication
- Best practices: Implement reconnection, heartbeat, rate limiting, authentication