WebSockets (JavaScript + Libraries + Python Servers)

    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)

    1. Client sends GET with Upgrade: websocket headers.
    2. Server responds 101 Switching Protocols.
    3. 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);
    });
    JavaScript

    Graceful close

    socket.close(1000, "Client done");
    JavaScript

    Binary 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);
      }
    });
    JavaScript

    Production-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);
    });
    JavaScript

    Room/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);
    });
    JavaScript

    Request-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);
      }
    });
    JavaScript

    4) Message Design Patterns

    Use a small envelope to standardize messages:

    {
        "type": "commentary.update",
        "id": "evt_123",
        "payload": { "text": "Goal by Team A" },
        "ts": 1730000000
    }
    JSON

    Communication 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" }
    JSON

    5) 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-redis
    Bash

    settings.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"}
    # }
    Python

    asgi.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)
            )
        ),
    })
    Python

    routing.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()),
    ]
    Python

    consumers.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"]
            }))
    Python

    consumers.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()
            }
    Python

    Token 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))
    Python

    Run

    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 8000
    Bash

    Management 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}'))
    Python

    Lifecycle in Django

    • connect() → authenticate, join groups, accept handshake
    • receive() → handle incoming messages, route by type
    • disconnect() → 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] websockets
    Bash

    Basic 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:
            pass
    Python

    Production 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()}
        }
    Python

    Sports 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]
    Python

    Background 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"})
    Python

    Run

    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 4
    Python

    Testing 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())
    Python

    Lifecycle in FastAPI

    • accept() → completes handshake
    • receive_text() / receive_json() → get messages
    • send_text() / send_json() → send messages
    • WebSocketDisconnect exception → 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;
    }
    JavaScript

    8) Heartbeats, Timeouts, and Cleanup

    Server‑side heartbeat (concept)

    • Send ping every N seconds
    • If no pong in 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);
    JavaScript

    9) 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: normal
    • 1006: 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");
    JavaScript

    CLI

    npx wscat -c ws://localhost:8000/ws
    JavaScript

    13) 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 GET request with Upgrade: websocket headers. If supported, the server responds with 101 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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *