WebSockets
The Problem
HTTP is request-response: the client asks, the server answers. But what if the server needs to tell the client something the client didn't ask for?
Chat app example:
User A sends a message → User B should see it instantly
With HTTP:
User B would need to ask: "Any new messages?"
Every 1 second... every 2 seconds...
→ Wasteful, slow, doesn't scaleThe Mental Model
Think of HTTP as a phone call where only one person can speak at a time. Every time you want to talk, you dial, say your piece, and hang up.
WebSockets are like a walkie-talkie. Both sides keep the channel open and can speak whenever they want.
HTTP: Client → "Hello?" → Server → "Hi!" → Hang up
Client → "Any news?" → Server → "No" → Hang up
Client → "Any news?" → Server → "Yes, here!" → Hang up
WebSocket: Client ←→ Open Channel ←→ Server
Server: "Hey, new message!" (no client asking)
Client: "Got it!" (instant)
Server: "And another one!" (instant)How WebSockets Work
Step 1: HTTP Upgrade Handshake
A WebSocket connection starts as a regular HTTP request, then "upgrades" to a WebSocket:
Client → Server:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Server → Client:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Connection is now a WebSocket. Both sides can send messages at any time.Step 2: Full-Duplex Communication
After the handshake:
Client ←→ WebSocket ←→ Server
Both sides can send messages independently.
No more request-response pattern.
Messages are small (2-14 bytes header vs HTTP's hundreds of bytes).WebSocket Frame Format
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) |
+---------------------------------------------------------------+Key points:
- FIN bit: Indicates this is the final fragment of a message
- Opcode: Tells the type (0x1 = text, 0x2 = binary, 0x8 = close, 0x9 = ping, 0xA = pong)
- Mask: Client-to-server messages MUST be masked (security)
- Payload: The actual data
WebSocket Implementation (Node.js)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('New client connected');
// Send welcome message
ws.send(JSON.stringify({ type: 'welcome', message: 'Connected!' }));
// Handle incoming messages
ws.on('message', (data) => {
const message = JSON.parse(data);
// Broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'chat',
user: message.user,
text: message.text,
timestamp: Date.now()
}));
}
});
});
// Handle disconnection
ws.on('close', () => {
console.log('Client disconnected');
});
// Handle errors
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
});WebSocket in Python
import asyncio
import websockets
connected_clients = set()
async def handler(websocket, path):
connected_clients.add(websocket)
try:
async for message in websocket:
# Broadcast to all connected clients
for client in connected_clients:
if client != websocket:
await client.send(message)
finally:
connected_clients.remove(websocket)
async def main():
async with websockets.serve(handler, "localhost", 8070):
await asyncio.Future() # Run forever
asyncio.run(main())WebSocket Scaling
The Challenge: WebSocket connections are stateful and persistent. You can't just round-robin them across servers.
Problem:
User A → Server 1 (WebSocket)
User B → Server 2 (WebSocket)
User A sends message to User B
Server 1 doesn't know User B is on Server 2!Solution: Pub/Sub with Redis
User A → Server 1 → Redis Pub/Sub → Server 2 → User B
1. Each WebSocket server subscribes to a Redis channel
2. When a message arrives, publish to Redis
3. Redis fans out to all subscribers
4. Servers deliver to relevant connected clientsimport redis
r = redis.Redis()
class WebSocketServer:
def __init__(self):
self.pubsub = r.pubsub()
self.pubsub.subscribe('chat_messages')
def handle_message(self, user_id, message):
# Publish to Redis
r.publish('chat_messages', json.dumps({
'user_id': user_id,
'message': message,
'server_id': self.server_id
}))
def listen_for_messages(self):
for item in self.pubsub.listen():
if item['type'] == 'message':
data = json.loads(item['data'])
# Deliver to connected clients on this server
self.deliver_to_local_clients(data)WebSocket Properties
| Property | Value |
|---|---|
| Connection | Persistent, full-duplex |
| Latency | Very low (~1ms overhead) |
| Server push | Native support |
| Binary data | Supported |
| Reconnection | Manual (must implement) |
| HTTP caching | Not available |
| Browser support | All modern browsers |
| Max connections | ~65K per server (file descriptor limit) |
When to Use WebSockets
- Chat applications: Slack, WhatsApp Web, Discord
- Real-time games: Multiplayer online games
- Live dashboards: Stock tickers, monitoring dashboards
- Collaborative editing: Google Docs, Figma
- Live notifications: Social media feeds, breaking news