WebSocket Architecture
Build real-time applications with WebSocket connections. Covers connection lifecycle, scaling WebSockets, heartbeat management, reconnection strategies, message protocols, and the patterns that enable real-time features like chat, notifications, and live updates.
WebSockets enable real-time, bidirectional communication between client and server. Unlike HTTP request-response where the client must poll for updates, WebSockets let the server push data to the client instantly. This powers chat, notifications, live dashboards, collaborative editing, and real-time gaming.
HTTP vs WebSocket
HTTP (Request-Response):
Client: "Any new messages?" → Server: "No"
Client: "Any new messages?" → Server: "No"
Client: "Any new messages?" → Server: "Yes, 1 message"
Polling interval: 1 second
99% of requests return nothing
High bandwidth waste, latency = polling interval
WebSocket (Bidirectional):
Client: "Open connection" → Server: "Connected ✓"
Server: "New message!" → Client: (instant)
Server: "User typing..." → Client: (instant)
Client: "Send: Hello" → Server: (instant)
Single long-lived connection
Server pushes data immediately
Zero wasted requests, near-zero latency
Server Implementation
# FastAPI WebSocket server
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import asyncio
import json
app = FastAPI()
class ConnectionManager:
"""Manage active WebSocket connections."""
def __init__(self):
self.connections: dict[str, list[WebSocket]] = {} # room -> connections
async def connect(self, websocket: WebSocket, room: str, user_id: str):
await websocket.accept()
if room not in self.connections:
self.connections[room] = []
self.connections[room].append(websocket)
# Notify room
await self.broadcast(room, {
"type": "user_joined",
"user_id": user_id,
"timestamp": datetime.utcnow().isoformat(),
})
async def disconnect(self, websocket: WebSocket, room: str):
self.connections[room].remove(websocket)
async def broadcast(self, room: str, message: dict):
"""Send message to all connections in a room."""
if room in self.connections:
dead_connections = []
for connection in self.connections[room]:
try:
await connection.send_json(message)
except Exception:
dead_connections.append(connection)
# Clean up dead connections
for conn in dead_connections:
self.connections[room].remove(conn)
manager = ConnectionManager()
@app.websocket("/ws/{room_id}")
async def websocket_endpoint(websocket: WebSocket, room_id: str):
user_id = websocket.query_params.get("user_id", "anonymous")
await manager.connect(websocket, room_id, user_id)
try:
while True:
data = await websocket.receive_json()
# Handle different message types
if data["type"] == "message":
await manager.broadcast(room_id, {
"type": "message",
"user_id": user_id,
"content": data["content"],
"timestamp": datetime.utcnow().isoformat(),
})
elif data["type"] == "typing":
await manager.broadcast(room_id, {
"type": "typing",
"user_id": user_id,
})
except WebSocketDisconnect:
await manager.disconnect(websocket, room_id)
Scaling WebSockets
Single Server:
Server holds all connections in memory
Works for < 10,000 connections
Problem: What if user A is on Server 1, user B on Server 2?
Multi-Server with Pub/Sub:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Server 1 │ │ Server 2 │ │ Server 3 │
│ 5K conns │ │ 5K conns │ │ 5K conns │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└───────────────┼───────────────┘
│
┌───────┴───────┐
│ Redis Pub/Sub │
│ (message bus) │
└───────────────┘
Server 1 receives message from User A
Publishes to Redis channel "room:123"
Server 2 and Server 3 receive from Redis
Forward to their connected users in room:123
Client Reconnection
class WebSocketClient {
constructor(url) {
this.url = url;
this.reconnectDelay = 1000;
this.maxDelay = 30000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectDelay = 1000; // Reset on success
};
this.ws.onclose = (event) => {
if (!event.wasClean) {
// Exponential backoff with jitter
const jitter = Math.random() * 1000;
const delay = Math.min(this.reconnectDelay + jitter, this.maxDelay);
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => this.connect(), delay);
this.reconnectDelay *= 2; // Double delay each attempt
}
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
}
}
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| No heartbeat/ping | Dead connections kept alive | Periodic ping/pong (every 30 seconds) |
| No reconnection logic | One disconnect = permanent loss | Exponential backoff with jitter |
| Store state only in WS memory | Lost on server restart | Persist to Redis/database |
| No authentication | Anyone can connect | Token-based auth on connection |
| Single server, no pub/sub | Cannot scale horizontally | Redis pub/sub for multi-server |
WebSockets are the right tool for real-time features. But they require more operational care than stateless HTTP — connection management, scaling, and reconnection must all be handled explicitly.