This guide explains WebSockets from fundamentals to production-ready patterns, with vanilla JavaScript client code, popular JavaScript libraries, and Python servers using Django and FastAPI. It also covers connection lifecycle, reliability, scaling, and best practices.
1) What is a WebSocket?
WebSocket is a persistent, full‑duplex connection over TCP that starts as HTTP and upgrades to the WebSocket protocol. It enables bi‑directional data flow without repeated HTTP handshakes.
Why not HTTP polling?
- Polling repeatedly asks, “Any updates?” — wasteful bandwidth and CPU.
- WebSockets keep a single connection open and push updates immediately.
Handshake (HTTP → WebSocket)
- Client sends
GETwithUpgrade: websocketheaders. - Server responds
101 Switching Protocols. - Connection becomes a WebSocket tunnel.
2) Connection Lifecycle (Client + Server)
WebSocket states:
- CONNECTING (0)
- OPEN (1)
- CLOSING (2)
- CLOSED (3)
Lifecycle events:
- open: handshake completed, ready for messages
- message: data received
- error: transport or protocol errors
- close: connection terminated
Common lifecycle concerns
- Ghost connections: client disappears without closing cleanly
- Heartbeat (ping/pong): detect dead clients and clean up
- Backpressure: client can’t process messages fast enough
- Reconnect: client tries to re‑establish after drop
3) Vanilla JavaScript WebSocket (Browser) – Comprehensive Guide
Basic client
const socket = new WebSocket("ws://localhost:8000/ws");
socket.addEventListener("open", () => {
console.log("Connected");
socket.send(JSON.stringify({ type: "hello", payload: "Hi server" }));
});
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
console.log("Message:", data);
});
socket.addEventListener("error", (err) => {
console.error("Socket error", err);
});
socket.addEventListener("close", (event) => {
console.log("Closed", event.code, event.reason);
});JavaScriptGraceful close
socket.close(1000, "Client done");JavaScriptBinary data
socket.binaryType = "arraybuffer";
socket.addEventListener("message", (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new Uint8Array(event.data);
console.log("Binary length", view.length);
}
});JavaScriptProduction-grade WebSocket client class
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectInterval: 1000,
maxReconnectInterval: 30000,
reconnectDecay: 1.5,
timeoutInterval: 2000,
maxReconnectAttempts: null,
...options
};
this.reconnectAttempts = 0;
this.readyState = WebSocket.CONNECTING;
this.messageQueue = [];
this.eventHandlers = {};
this.heartbeatInterval = null;
this.reconnectTimeout = null;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.setupEventListeners();
}
setupEventListeners() {
this.ws.onopen = (event) => {
console.log('WebSocket connected');
this.readyState = WebSocket.OPEN;
this.reconnectAttempts = 0;
// Send queued messages
while (this.messageQueue.length > 0) {
const msg = this.messageQueue.shift();
this.ws.send(msg);
}
// Start heartbeat
this.startHeartbeat();
this.trigger('open', event);
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Handle heartbeat pong
if (data.type === 'pong') {
return;
}
this.trigger('message', data);
} catch (e) {
// Handle binary or non-JSON data
this.trigger('message', event.data);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.trigger('error', error);
};
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.readyState = WebSocket.CLOSED;
this.stopHeartbeat();
this.trigger('close', event);
// Attempt reconnect
if (this.shouldReconnect(event)) {
this.reconnect();
}
};
}
shouldReconnect(event) {
const { maxReconnectAttempts } = this.options;
// Don't reconnect if closed normally or max attempts reached
if (event.code === 1000) return false;
if (maxReconnectAttempts && this.reconnectAttempts >= maxReconnectAttempts) {
return false;
}
return true;
}
reconnect() {
this.reconnectAttempts++;
const timeout = Math.min(
this.options.reconnectInterval * Math.pow(this.options.reconnectDecay, this.reconnectAttempts),
this.options.maxReconnectInterval
);
console.log(`Reconnecting in ${timeout}ms (attempt ${this.reconnectAttempts})`);
this.reconnectTimeout = setTimeout(() => {
console.log('Reconnecting...');
this.connect();
}, timeout);
}
send(data) {
const message = typeof data === 'string' ? data : JSON.stringify(data);
if (this.readyState === WebSocket.OPEN) {
this.ws.send(message);
} else {
// Queue message for later
this.messageQueue.push(message);
}
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.readyState === WebSocket.OPEN) {
this.send({ type: 'ping', timestamp: Date.now() });
}
}, 30000); // 30 seconds
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
on(event, callback) {
if (!this.eventHandlers[event]) {
this.eventHandlers[event] = [];
}
this.eventHandlers[event].push(callback);
}
off(event, callback) {
if (!this.eventHandlers[event]) return;
this.eventHandlers[event] = this.eventHandlers[event].filter(
handler => handler !== callback
);
}
trigger(event, data) {
if (!this.eventHandlers[event]) return;
this.eventHandlers[event].forEach(handler => {
try {
handler(data);
} catch (e) {
console.error(`Error in ${event} handler:`, e);
}
});
}
close(code = 1000, reason = 'Client closing') {
this.stopHeartbeat();
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
if (this.ws) {
this.ws.close(code, reason);
}
}
}
// Usage
const client = new WebSocketClient('ws://localhost:8000/ws', {
reconnectInterval: 1000,
maxReconnectAttempts: 10
});
client.on('open', () => {
console.log('Connected to server');
client.send({ type: 'subscribe', channel: 'sports' });
});
client.on('message', (data) => {
console.log('Received:', data);
});
client.on('close', (event) => {
console.log('Connection closed:', event);
});
client.on('error', (error) => {
console.error('Error:', error);
});JavaScriptRoom/Channel subscription pattern
class RoomManager {
constructor(wsClient) {
this.client = wsClient;
this.subscriptions = new Set();
this.handlers = new Map();
this.client.on('message', (data) => this.handleMessage(data));
}
subscribe(room, handler) {
if (!this.subscriptions.has(room)) {
this.client.send({
type: 'subscribe',
room: room
});
this.subscriptions.add(room);
}
if (!this.handlers.has(room)) {
this.handlers.set(room, []);
}
this.handlers.get(room).push(handler);
}
unsubscribe(room, handler) {
const handlers = this.handlers.get(room);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
if (handlers.length === 0) {
this.client.send({
type: 'unsubscribe',
room: room
});
this.subscriptions.delete(room);
this.handlers.delete(room);
}
}
}
handleMessage(data) {
if (data.room && this.handlers.has(data.room)) {
const handlers = this.handlers.get(data.room);
handlers.forEach(handler => handler(data));
}
}
}
// Usage
const wsClient = new WebSocketClient('ws://localhost:8000/ws');
const roomManager = new RoomManager(wsClient);
roomManager.subscribe('match:123', (data) => {
console.log('Match 123 update:', data);
});
roomManager.subscribe('match:456', (data) => {
console.log('Match 456 update:', data);
});JavaScriptRequest-Response pattern with acknowledgments
class WebSocketRPC {
constructor(wsClient) {
this.client = wsClient;
this.pendingRequests = new Map();
this.requestId = 0;
this.client.on('message', (data) => this.handleResponse(data));
}
request(method, params, timeout = 5000) {
return new Promise((resolve, reject) => {
const id = ++this.requestId;
const timer = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error('Request timeout'));
}, timeout);
this.pendingRequests.set(id, { resolve, reject, timer });
this.client.send({
jsonrpc: '2.0',
id: id,
method: method,
params: params
});
});
}
handleResponse(data) {
if (data.jsonrpc === '2.0' && data.id) {
const pending = this.pendingRequests.get(data.id);
if (pending) {
clearTimeout(pending.timer);
this.pendingRequests.delete(data.id);
if (data.error) {
pending.reject(new Error(data.error.message));
} else {
pending.resolve(data.result);
}
}
}
}
}
// Usage
const wsClient = new WebSocketClient('ws://localhost:8000/ws');
const rpc = new WebSocketRPC(wsClient);
wsClient.on('open', async () => {
try {
const result = await rpc.request('getMatches', { status: 'live' });
console.log('Live matches:', result);
} catch (error) {
console.error('RPC error:', error);
}
});JavaScript4) Message Design Patterns
Use a small envelope to standardize messages:
{
"type": "commentary.update",
"id": "evt_123",
"payload": { "text": "Goal by Team A" },
"ts": 1730000000
}JSONCommunication patterns
- Unicast: one client
- Broadcast: all clients
- Multicast/Rooms: subset (topic, match, room)
- Pub/Sub: clients subscribe to topics
Reliability with ACKs
If message delivery matters, use acknowledgments:
{ "type": "ack", "id": "evt_123" }JSON5) WebSocket Libraries (JavaScript)
Server‑side (Node.js)
- ws: minimal, fast, most common
- uWebSockets.js: high performance, C++ bindings
- socket.io: feature‑rich (fallbacks, rooms, acks), not raw WebSocket protocol
Client‑side
- native WebSocket API (built‑in)
- socket.io‑client (paired with socket.io server)
- reconnecting‑websocket (auto reconnect helper)
CLI tools
- wscat: interactive testing
- websocat: advanced CLI for scripts
6) Python WebSocket Servers
Below are minimal, production‑ready patterns for Django and FastAPI.
A) Django (with Channels) – Comprehensive Implementation
WebSockets in Django are usually done with Django Channels (ASGI).
Install
pip install channels channels-redisBashsettings.py
INSTALLED_APPS = [
"daphne", # Must be first
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"channels",
"myapp",
]
ASGI_APPLICATION = "myproject.asgi.application"
# Production: Redis channel layer
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
# Development: In-memory channel layer
# CHANNEL_LAYERS = {
# "default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}
# }Pythonasgi.py
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
# Initialize Django ASGI application early
django_asgi_app = get_asgi_application()
from myapp.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
)
),
})Pythonrouting.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
re_path(r"ws/notifications/$", consumers.NotificationConsumer.as_asgi()),
re_path(r"ws/sports/match/(?P<match_id>\d+)/$", consumers.SportsConsumer.as_asgi()),
]Pythonconsumers.py – Basic Sync Consumer
import json
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
self.room_group_name = f"chat_{self.room_name}"
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
# Send welcome message
self.send(text_data=json.dumps({
"type": "connection_established",
"message": f"You joined {self.room_name}"
}))
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
def receive(self, text_data=None, bytes_data=None):
if text_data:
data = json.loads(text_data)
message_type = data.get("type")
if message_type == "chat_message":
# Send message to room group
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
"type": "chat_message",
"message": data["message"],
"username": self.scope["user"].username,
"timestamp": data.get("timestamp")
}
)
# Receive message from room group
def chat_message(self, event):
# Send message to WebSocket
self.send(text_data=json.dumps({
"type": "chat_message",
"message": event["message"],
"username": event["username"],
"timestamp": event["timestamp"]
}))Pythonconsumers.py – Async Consumer (Production)
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
User = get_user_model()
class SportsConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.match_id = self.scope["url_route"]["kwargs"]["match_id"]
self.room_group_name = f"match_{self.match_id}"
self.user = self.scope["user"]
# Authentication check
if not self.user.is_authenticated:
await self.close(code=4001)
return
# Join match group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Send initial match data
match_data = await self.get_match_data(self.match_id)
await self.send(text_data=json.dumps({
"type": "match_data",
"data": match_data
}))
async def disconnect(self, close_code):
# Leave match group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data=None, bytes_data=None):
if text_data:
try:
data = json.loads(text_data)
message_type = data.get("type")
if message_type == "subscribe":
await self.handle_subscribe(data)
elif message_type == "commentary":
await self.handle_commentary(data)
elif message_type == "ping":
await self.send(text_data=json.dumps({"type": "pong"}))
except json.JSONDecodeError:
await self.send(text_data=json.dumps({
"type": "error",
"message": "Invalid JSON"
}))
async def handle_subscribe(self, data):
# Additional subscription logic
await self.send(text_data=json.dumps({
"type": "subscribed",
"match_id": self.match_id
}))
async def handle_commentary(self, data):
# Save commentary to database
commentary = await self.save_commentary(
self.match_id,
data["text"],
self.user
)
# Broadcast to all in match group
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "commentary_update",
"commentary": commentary
}
)
# Receive commentary from group
async def commentary_update(self, event):
await self.send(text_data=json.dumps({
"type": "commentary",
"data": event["commentary"]
}))
# Database operations
@database_sync_to_async
def get_match_data(self, match_id):
from .models import Match
match = Match.objects.get(id=match_id)
return {
"id": match.id,
"home_team": match.home_team,
"away_team": match.away_team,
"score": match.score,
"status": match.status
}
@database_sync_to_async
def save_commentary(self, match_id, text, user):
from .models import Commentary
commentary = Commentary.objects.create(
match_id=match_id,
text=text,
user=user
)
return {
"id": commentary.id,
"text": commentary.text,
"username": user.username,
"timestamp": commentary.created_at.isoformat()
}PythonToken Authentication Middleware
# middleware.py
from channels.db import database_sync_to_async
from channels.middleware import BaseMiddleware
from django.contrib.auth.models import AnonymousUser
from urllib.parse import parse_qs
class TokenAuthMiddleware(BaseMiddleware):
async def __call__(self, scope, receive, send):
query_string = scope.get("query_string", b"").decode()
params = parse_qs(query_string)
token = params.get("token", [None])[0]
if token:
scope["user"] = await self.get_user_from_token(token)
else:
scope["user"] = AnonymousUser()
return await super().__call__(scope, receive, send)
@database_sync_to_async
def get_user_from_token(self, token):
from rest_framework.authtoken.models import Token
try:
return Token.objects.get(key=token).user
except Token.DoesNotExist:
return AnonymousUser()
# Use in asgi.py:
# from myapp.middleware import TokenAuthMiddleware
# "websocket": TokenAuthMiddleware(URLRouter(websocket_urlpatterns))PythonRun
pip install daphne
daphne -p 8000 myproject.asgi:application
# Or with uvicorn
pip install uvicorn
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000BashManagement Command for Broadcasting
# management/commands/broadcast_update.py
from django.core.management.base import BaseCommand
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
class Command(BaseCommand):
help = 'Broadcast update to match group'
def add_arguments(self, parser):
parser.add_argument('match_id', type=int)
parser.add_argument('message', type=str)
def handle(self, *args, **options):
channel_layer = get_channel_layer()
match_id = options['match_id']
message = options['message']
async_to_sync(channel_layer.group_send)(
f"match_{match_id}",
{
"type": "commentary_update",
"commentary": {
"text": message,
"timestamp": "now"
}
}
)
self.stdout.write(self.style.SUCCESS(f'Broadcast to match {match_id}'))PythonLifecycle in Django
connect()→ authenticate, join groups, accept handshakereceive()→ handle incoming messages, route by typedisconnect()→ leave groups, cleanup- Group handlers (e.g.,
chat_message) → receive broadcasts from channel layer
B) FastAPI (native WebSocket support) – Comprehensive Implementation
FastAPI supports WebSockets out of the box with excellent async support.
Install
pip install fastapi uvicorn python-jose[cryptography] websocketsBashBasic WebSocket
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
try:
while True:
data = await ws.receive_text()
await ws.send_text(f"Echo: {data}")
except WebSocketDisconnect:
passPythonProduction WebSocket with Connection Manager
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, status, Query
from typing import Dict, List, Set, Optional
import json
import asyncio
from datetime import datetime
app = FastAPI()
class ConnectionManager:
def __init__(self):
# Store all active connections
self.active_connections: List[WebSocket] = []
# Store connections by room/topic
self.rooms: Dict[str, Set[WebSocket]] = {}
# Store user metadata
self.connection_metadata: Dict[WebSocket, dict] = {}
async def connect(self, websocket: WebSocket, user_id: Optional[str] = None):
await websocket.accept()
self.active_connections.append(websocket)
self.connection_metadata[websocket] = {
"user_id": user_id,
"connected_at": datetime.now(),
"subscriptions": set()
}
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
# Remove from all rooms
metadata = self.connection_metadata.get(websocket, {})
for room in metadata.get("subscriptions", set()):
self.leave_room(websocket, room)
# Clean up metadata
if websocket in self.connection_metadata:
del self.connection_metadata[websocket]
def join_room(self, websocket: WebSocket, room: str):
if room not in self.rooms:
self.rooms[room] = set()
self.rooms[room].add(websocket)
if websocket in self.connection_metadata:
self.connection_metadata[websocket]["subscriptions"].add(room)
def leave_room(self, websocket: WebSocket, room: str):
if room in self.rooms:
self.rooms[room].discard(websocket)
if not self.rooms[room]: # Remove empty room
del self.rooms[room]
if websocket in self.connection_metadata:
self.connection_metadata[websocket]["subscriptions"].discard(room)
async def send_personal_message(self, message: str, websocket: WebSocket):
await websocket.send_text(message)
async def send_json(self, data: dict, websocket: WebSocket):
await websocket.send_json(data)
async def broadcast(self, message: str):
"""Send to all connected clients"""
for connection in self.active_connections:
try:
await connection.send_text(message)
except Exception as e:
print(f"Error broadcasting: {e}")
async def broadcast_json(self, data: dict):
"""Send JSON to all connected clients"""
for connection in self.active_connections:
try:
await connection.send_json(data)
except Exception as e:
print(f"Error broadcasting: {e}")
async def broadcast_to_room(self, room: str, data: dict):
"""Send to all clients in a specific room"""
if room not in self.rooms:
return
disconnected = []
for connection in self.rooms[room]:
try:
await connection.send_json(data)
except Exception as e:
print(f"Error sending to room {room}: {e}")
disconnected.append(connection)
# Clean up disconnected clients
for connection in disconnected:
self.disconnect(connection)
def get_room_size(self, room: str) -> int:
return len(self.rooms.get(room, set()))
manager = ConnectionManager()
# Authentication dependency
async def get_current_user(token: Optional[str] = Query(None)):
"""Validate token and return user info"""
if not token:
return None
# Validate token (implement your auth logic)
# For demo, just return token as user_id
return {"user_id": token, "username": f"user_{token}"}
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(
websocket: WebSocket,
client_id: str,
user: Optional[dict] = Depends(get_current_user)
):
user_id = user.get("user_id") if user else None
await manager.connect(websocket, user_id=user_id)
# Send welcome message
await manager.send_json({
"type": "connection_established",
"client_id": client_id,
"user_id": user_id,
"timestamp": datetime.now().isoformat()
}, websocket)
try:
while True:
# Receive message
data = await websocket.receive_text()
message = json.loads(data)
# Handle different message types
msg_type = message.get("type")
if msg_type == "subscribe":
room = message.get("room")
manager.join_room(websocket, room)
await manager.send_json({
"type": "subscribed",
"room": room,
"members": manager.get_room_size(room)
}, websocket)
elif msg_type == "unsubscribe":
room = message.get("room")
manager.leave_room(websocket, room)
await manager.send_json({
"type": "unsubscribed",
"room": room
}, websocket)
elif msg_type == "message":
room = message.get("room")
await manager.broadcast_to_room(room, {
"type": "message",
"room": room,
"user_id": user_id,
"content": message.get("content"),
"timestamp": datetime.now().isoformat()
})
elif msg_type == "ping":
await manager.send_json({
"type": "pong",
"timestamp": datetime.now().isoformat()
}, websocket)
except WebSocketDisconnect:
manager.disconnect(websocket)
print(f"Client {client_id} disconnected")
except Exception as e:
print(f"Error: {e}")
manager.disconnect(websocket)
# REST endpoint to broadcast to a room
@app.post("/broadcast/{room}")
async def broadcast_to_room(room: str, message: dict):
await manager.broadcast_to_room(room, {
"type": "broadcast",
"room": room,
"data": message,
"timestamp": datetime.now().isoformat()
})
return {"status": "sent", "room": room, "recipients": manager.get_room_size(room)}
@app.get("/rooms")
async def list_rooms():
return {
"rooms": [
{"name": room, "members": len(connections)}
for room, connections in manager.rooms.items()
]
}
@app.get("/stats")
async def get_stats():
return {
"total_connections": len(manager.active_connections),
"total_rooms": len(manager.rooms),
"rooms": {room: len(conns) for room, conns in manager.rooms.items()}
}PythonSports Match Example (Production Pattern)
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
from pydantic import BaseModel
from typing import Optional
import asyncio
app = FastAPI()
class Match(BaseModel):
id: int
home_team: str
away_team: str
score: str
status: str
class Commentary(BaseModel):
match_id: int
text: str
timestamp: str
# In-memory storage (use database in production)
matches_db: Dict[int, Match] = {}
commentary_db: List[Commentary] = []
class SportsConnectionManager:
def __init__(self):
self.match_subscribers: Dict[int, Set[WebSocket]] = {}
async def connect(self, websocket: WebSocket, match_id: int):
await websocket.accept()
if match_id not in self.match_subscribers:
self.match_subscribers[match_id] = set()
self.match_subscribers[match_id].add(websocket)
def disconnect(self, websocket: WebSocket, match_id: int):
if match_id in self.match_subscribers:
self.match_subscribers[match_id].discard(websocket)
if not self.match_subscribers[match_id]:
del self.match_subscribers[match_id]
async def broadcast_to_match(self, match_id: int, data: dict):
if match_id not in self.match_subscribers:
return
disconnected = []
for connection in self.match_subscribers[match_id]:
try:
await connection.send_json(data)
except:
disconnected.append(connection)
for connection in disconnected:
self.disconnect(connection, match_id)
sports_manager = SportsConnectionManager()
@app.websocket("/ws/match/{match_id}")
async def match_websocket(websocket: WebSocket, match_id: int):
if match_id not in matches_db:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
await sports_manager.connect(websocket, match_id)
# Send initial match data
await websocket.send_json({
"type": "match_data",
"data": matches_db[match_id].dict()
})
try:
while True:
data = await websocket.receive_json()
if data.get("type") == "ping":
await websocket.send_json({"type": "pong"})
except WebSocketDisconnect:
sports_manager.disconnect(websocket, match_id)
@app.post("/matches")
async def create_match(match: Match):
matches_db[match.id] = match
return match
@app.post("/matches/{match_id}/commentary")
async def add_commentary(match_id: int, commentary: Commentary):
if match_id not in matches_db:
raise HTTPException(status_code=404, detail="Match not found")
commentary_db.append(commentary)
# Broadcast to all subscribers
await sports_manager.broadcast_to_match(match_id, {
"type": "commentary",
"data": commentary.dict()
})
return commentary
@app.put("/matches/{match_id}/score")
async def update_score(match_id: int, score: str):
if match_id not in matches_db:
raise HTTPException(status_code=404, detail="Match not found")
matches_db[match_id].score = score
# Broadcast to all subscribers
await sports_manager.broadcast_to_match(match_id, {
"type": "score_update",
"data": {"match_id": match_id, "score": score}
})
return matches_db[match_id]PythonBackground Task for Heartbeat
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.on_event("startup")
async def startup_event():
asyncio.create_task(heartbeat_task())
async def heartbeat_task():
while True:
await asyncio.sleep(30)
# Send ping to all connections
await manager.broadcast_json({"type": "ping"})PythonRun
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
# Production with multiple workers
uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4PythonTesting with Python Client
import asyncio
import websockets
import json
async def test_client():
uri = "ws://localhost:8000/ws/client123?token=mytoken"
async with websockets.connect(uri) as websocket:
# Subscribe to a room
await websocket.send(json.dumps({
"type": "subscribe",
"room": "match:456"
}))
# Listen for messages
async for message in websocket:
data = json.loads(message)
print(f"Received: {data}")
if data["type"] == "ping":
await websocket.send(json.dumps({"type": "pong"}))
asyncio.run(test_client())PythonLifecycle in FastAPI
accept()→ completes handshakereceive_text()/receive_json()→ get messagessend_text()/send_json()→ send messagesWebSocketDisconnectexception → cleanup on disconnect- Connection manager handles room subscriptions and broadcasting
7) Serving WebSockets in Production (Python)
Use ASGI servers
- Uvicorn (FastAPI, Starlette)
- Daphne (Django Channels)
- Hypercorn (ASGI + HTTP/2)
Reverse proxy
- Nginx or Caddy to handle TLS and pass upgrade headers
Nginx snippet:
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}JavaScript8) Heartbeats, Timeouts, and Cleanup
Server‑side heartbeat (concept)
- Send
pingevery N seconds - If no
pongin time, terminate
Client‑side heartbeat (browser)
Browsers do not expose raw ping/pong, so send app‑level heartbeats:
setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "ping" }));
}
}, 30000);JavaScript9) Backpressure & Message Limits
Symptoms:
- memory grows
- delayed UI updates
Mitigations:
- cap send queue
- drop or coalesce updates
- compress payloads
- switch to binary for heavy data
10) Security Basics
- Origin checks (prevent CSRF‑like abuse)
- Auth tokens in query params or headers
- Rate limiting handshake + messages
- Close codes for policy violations
Common close codes:
1000: normal1006: abnormal (no close frame)1008: policy violation
11) When to Use WebSockets (vs SSE/WebRTC)
- WebSockets: bidirectional, chat, collaborative apps, live dashboards
- SSE: server → client only (simpler)
- WebRTC: peer‑to‑peer media (audio/video)
- WebTransport: ultra‑low latency multi‑stream over QUIC
12) Minimal Testing Tools
Browser console
new WebSocket("ws://localhost:8000/ws");JavaScriptCLI
npx wscat -c ws://localhost:8000/wsJavaScript13) Quick Checklist for Production
- Use ASGI server
- Configure reverse proxy for upgrade headers
- Add heartbeat + cleanup
- Authenticate connections
- Use room/topic routing
- Add metrics/monitoring
- Handle backpressure
14) Glossary
- Handshake: HTTP upgrade to WebSocket
- Frame: WebSocket message unit
- Opcode: frame type (text/binary/ping/pong)
- Full‑duplex: send and receive simultaneously
If you want, I can add real‑world examples (chat, sports updates, collaborative docs) or ready‑to‑run templates for Django and FastAPI.
Based on the sources provided, here are several diagrams created with Mermaid.js to illustrate the core concepts, lifecycle, and architecture of WebSockets.
1. The WebSocket Handshake Process
The connection begins as a standard HTTP request and upgrades to the WebSocket protocol through a specific handshake,.
sequenceDiagram
participant Client
participant Server
Note over Client,Server: Phase 1: HTTP Handshake
Client->>Server: GET /ws HTTP/1.1
Note right of Client: Upgrade: websocketConnection: Upgrade
Server->>Client: HTTP/1.1 101 Switching Protocols
Note left of Server: Upgrade: websocketConnection: Upgrade
Note over Client,Server: Phase 2: WebSocket Tunnel Established
rect rgb(240, 240, 240)
Client->>Server: Full-Duplex Data (Binary/JSON)
Server->>Client: Full-Duplex Data (Binary/JSON)
end- Description: The client sends a
GETrequest withUpgrade: websocketheaders. If supported, the server responds with101 Switching Protocols, at which point the HTTP connection ends and the WebSocket tunnel remains open,.
2. Connection Lifecycle States
A WebSocket is a state machine with four distinct stages. It is critical not to send data unless the socket is in the OPEN state,.
stateDiagram-v2
[*] --> CONNECTING: Handshake starts (0)
CONNECTING --> OPEN: Handshake complete (1)
OPEN --> CLOSING: Termination started (2)
OPEN --> OPEN: message / error / ping-pong
CLOSING --> CLOSED: Connection dead (3)
CLOSED --> [*]
note right of OPEN: Safe zone for data transfer- States:
- CONNECTING (0): The handshake is still in progress,.
- OPEN (1): The tunnel is live and messages can be sent safely,.
- CLOSING (2): The connection is shutting down,.
- CLOSED (3): The connection is dead; a new instance must be created to reconnect,.
3. Communication Patterns
WebSockets support various routing patterns to determine which clients receive specific updates,.
graph TD
subgraph Patterns
A[Server] -->|Unicast| B(One Specific Client)
A -->|Broadcast| C{All Connected Clients}
A -->|Multicast/Rooms| D[Subset of Clients in Room]
end
subgraph Pub_Sub_Scaling
E[Server 1] <--> G[(Message Broker/Redis)]
F[Server 2] <--> G
G -->|Sync| H[Client on Server 1]
G -->|Sync| I[Client on Server 2]
end- Unicast: Targeted 1-to-1 communication, often used for private messages or notifications,.
- Broadcast: One-to-all communication used for global system announcements,.
- Multicast/Rooms: Messaging restricted to specific groups or topics, such as a specific sports match room,.
- Pub/Sub & Scaling: In production, a message broker like Redis is used to sync updates across multiple server instances.
4. Heartbeat (Ping/Pong) Architecture
To prevent ghost connections—where a server keeps a dead connection in memory because it didn’t close cleanly—servers use heartbeats,.
sequenceDiagram
participant Server
participant Client
loop Every 30 Seconds
Server->>Client: Ping (Tiny frame)
Client->>Server: Pong (Response)
end
Note over Server: No Pong received?
Server->>Server: Terminate Socket (Clean memory)- Function: The server sends a tiny “ping” impulse; if the client does not respond with a “pong,” the server terminates the socket to prevent memory leaks,.
5. Technology Decision Matrix
Choosing between WebSockets and other real-time technologies depends on the direction of data and latency needs,.
graph TD
Start{Does the server need to push updates?}
Start -->|No| HTTP[Standard HTTP]
Start -->|Yes| ClientTalk{Does the client need to talk back?}
ClientTalk -->|No| SSE[SSE - Server-Sent Events]
ClientTalk -->|Yes| HeavyMedia{Is it heavy audio/video/media?}
HeavyMedia -->|Yes| WebRTC[WebRTC - Peer-to-Peer]
HeavyMedia -->|No| UltraLow[Ultra-low latency required?]
UltraLow -->|Yes| WebTransport[WebTransport - over QUIC]
UltraLow -->|No| WS[WebSockets]- WebSockets: Best for bidirectional apps like chat, live dashboards, and collaboration,.
- SSE: Simplified one-way stream from server to client, ideal for news feeds or stock tickers,.
- WebRTC: Peer-to-peer communication for high-bandwidth media like video calls,.
- WebTransport: A modern protocol for ultra-low latency using multiple streams over QUIC,.
Discover more from Altgr Blog
Subscribe to get the latest posts sent to your email.
