WebSocket & Real-Time Architecture
Build real-time applications. Covers WebSocket architecture, SSE, long polling, real-time database patterns, connection scaling, and choosing the right real-time protocol.
HTTP is request-response: the client asks, the server answers. But many applications need the server to push data to the client without being asked — live scores, chat messages, stock prices, collaborative editing. Real-time architecture flips the communication model from pull to push.
Protocol Comparison
| Protocol | Direction | Latency | Complexity | Best For |
|---|---|---|---|---|
| WebSocket | Bidirectional | Very low | Medium | Chat, gaming, collaboration |
| Server-Sent Events (SSE) | Server → Client | Low | Low | Live feeds, notifications |
| Long Polling | Simulated push | Medium | Low | Legacy browser support |
| WebTransport | Bidirectional (QUIC) | Very low | High | Low-latency, unreliable delivery OK |
| gRPC Streaming | Bidirectional | Low | Medium | Microservice communication |
WebSocket Architecture
Load Balancer (sticky sessions or shared state)
┌──────────┴──────────┐
┌────▼────┐ ┌────▼────┐
│ App │ │ App │
│ Server 1│◀── pub ──│ Server 2│
│ │── sub ──▶│ │
└────┬────┘ └────┬────┘
│ │
┌────▼─────────────────────▼────┐
│ Redis Pub/Sub │
│ (message broadcast layer) │
└───────────────────────────────┘
Connection Management
import asyncio
import websockets
import json
connected_clients = set()
async def handler(websocket, path):
# Register
connected_clients.add(websocket)
try:
async for message in websocket:
data = json.loads(message)
# Broadcast to all connected clients
if data["type"] == "chat":
await broadcast(json.dumps({
"type": "chat",
"user": data["user"],
"message": data["message"],
"timestamp": time.time()
}))
finally:
# Unregister on disconnect
connected_clients.discard(websocket)
async def broadcast(message):
if connected_clients:
await asyncio.gather(
*[client.send(message) for client in connected_clients]
)
Server-Sent Events (SSE)
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.get("/events")
async def event_stream():
"""Server-Sent Events endpoint."""
async def generate():
while True:
data = await get_latest_update()
yield f"event: update\ndata: {json.dumps(data)}\n\n"
await asyncio.sleep(1)
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
)
// Client-side SSE (auto-reconnect built in!)
const source = new EventSource('/events');
source.addEventListener('update', (event) => {
const data = JSON.parse(event.data);
updateUI(data);
});
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Polling every second | Wastes bandwidth, battery, server CPU | SSE or WebSocket for server-push |
| WebSocket for one-way data | Over-engineering | SSE for server→client only |
| No heartbeat/ping | Dead connections held open | Periodic ping/pong, timeout detection |
| All data through WebSocket | Large payloads block real-time messages | WebSocket for events, HTTP for data fetches |
| No reconnection logic | Dropped connections = broken app | Auto-reconnect with backoff |
Checklist
- Protocol selected based on use case (WebSocket vs SSE vs polling)
- Connection scaling: Redis pub/sub or similar for multi-instance
- Load balancer: sticky sessions or connection-aware routing
- Heartbeat: ping/pong to detect dead connections
- Reconnection: client auto-reconnects with backoff
- Authentication: validated on initial connection
- Rate limiting: per-connection message rate limits
- Monitoring: active connections, message throughput
:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For real-time architecture consulting, visit garnetgrid.com. :::