Skip to main content

WebSocket Protocol: Persistent Connections 2026 — Complete Guide with Python and JavaScript

Created: March 11, 2026 Larry Qu 5 min read

Introduction

WebSocket (RFC 6455) provides full-duplex communication over a single TCP connection, unlike HTTP’s request-response model. After an HTTP Upgrade handshake, the connection switches to WebSocket framing with minimal overhead — no HTTP headers on each message. This makes it the standard for real-time applications: chat, live dashboards, financial tickers, collaborative editing, and online gaming.

This guide covers the WebSocket handshake with a Mermaid sequence diagram, JavaScript client API with explained event handlers, Python async server with connection management and broadcasting, Nginx reverse proxy for WSS (WebSocket Secure), and production considerations including subprotocol negotiation, heartbeat/ping-pong, and auto-reconnection.

WebSocket Handshake

The handshake starts as an HTTP request with an Upgrade header and switches protocols:

sequenceDiagram
    participant Client
    participant Server

    Note over Client,Server: HTTP Upgrade Handshake
    Client->>Server: GET /chat HTTP/1.1
    Client->>Server: Host: server.example.com
    Client->>Server: Upgrade: websocket
    Client->>Server: Connection: Upgrade
    Client->>Server: Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Client->>Server: Sec-WebSocket-Version: 13

    Server-->>Client: HTTP/1.1 101 Switching Protocols
    Server-->>Client: Upgrade: websocket
    Server-->>Client: Connection: Upgrade
    Server-->>Client: Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

    Note over Client,Server: Connection upgraded to WebSocket
    Note over Client,Server: Full-duplex communication begins

The Sec-WebSocket-Key is a random 16-byte value base64-encoded. The server concatenates it with a fixed GUID (258EAFA5-E914-47DA-95CA-C5AB0DC85B11), computes SHA-1, and returns the base64 result as Sec-WebSocket-Accept. This confirms the server actually understands WebSocket, not just any HTTP server.

JavaScript Client

The browser’s WebSocket API provides event-driven connection management. The key events are onopen (connection established), onmessage (data received), onerror (connection error), and onclose (disconnection with code and reason):

// Client-side WebSocket with auto-reconnection
class ReconnectingWebSocket {
    constructor(url, options = {}) {
        this.url = url;
        this.reconnectInterval = options.reconnectInterval || 3000;
        this.maxRetries = options.maxRetries || 10;
        this.retries = 0;
        this.connect();
    }

    connect() {
        this.ws = new WebSocket(this.url);

        this.ws.onopen = () => {
            console.log('WebSocket connected');
            this.retries = 0;
            // Join a room immediately after connecting
            this.send(JSON.stringify({ type: 'join', room: 'lobby' }));
        };

        this.ws.onmessage = (event) => {
            // event.data contains the message (string or Blob)
            const data = JSON.parse(event.data);
            this.handleMessage(data);
        };

        this.ws.onerror = (error) => {
            console.error('WebSocket error:', error);
        };

        this.ws.onclose = (event) => {
            // event.code: 1000=normal, 1006=abnormal disconnect
            // event.reason: human-readable reason string
            console.log(`Disconnected: code=${event.code} reason=${event.reason}`);

            if (this.retries < this.maxRetries) {
                this.retries++;
                setTimeout(() => this.connect(), this.reconnectInterval);
            }
        };
    }

    send(data) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(data);
        }
    }

    handleMessage(data) {
        // Dispatch by message type
        switch (data.type) {
            case 'message': console.log('Message:', data.payload); break;
            case 'presence': this.updateUserList(data.users); break;
            case 'pong': /* heartbeat response */ break;
        }
    }

    close() {
        this.maxRetries = 0;  // prevent reconnect
        this.ws.close(1000, 'Client closing');
    }
}

const ws = new ReconnectingWebSocket('wss://chat.example.com');

Python Server with websockets Library

The Python websockets library provides an async server based on asyncio. The handler coroutine manages a single client connection, receiving messages in a loop and broadcasting to other connected clients:

import asyncio
import json
import websockets

# Track all connected clients for broadcasting
connected_clients = set()

async def chat_handler(websocket):
    """Handle a single WebSocket connection.

    Registers the client in the connected set, then loops
    receiving messages until the connection closes.
    """
    connected_clients.add(websocket)
    try:
        async for message in websocket:
            data = json.loads(message)

            if data['type'] == 'message':
                # Broadcast message to all connected clients
                await broadcast(json.dumps({
                    'type': 'message',
                    'sender': id(websocket),
                    'payload': data['payload']
                }))

            elif data['type'] == 'ping':
                # Respond to heartbeat ping
                await websocket.send(json.dumps({'type': 'pong'}))

    except websockets.exceptions.ConnectionClosed:
        print(f"Client {id(websocket)} disconnected")
    finally:
        connected_clients.discard(websocket)

async def broadcast(message: str):
    """Send a message to all connected clients.

    Uses asyncio.gather to send to all clients concurrently.
    Failed sends are logged but don't block other clients.
    """
    if not connected_clients:
        return
    await asyncio.gather(
        *(client.send(message) for client in connected_clients),
        return_exceptions=True
    )

async def main():
    async with websockets.serve(chat_handler, "0.0.0.0", 8765):
        print("WebSocket server running on ws://0.0.0.0:8765")
        await asyncio.Future()  # Run forever

asyncio.run(main())

Nginx Reverse Proxy for WSS

WebSocket connections need special proxy configuration because standard HTTP proxies don’t understand the Upgrade mechanism. Nginx requires explicit Upgrade and Connection headers:

# /etc/nginx/sites-available/websocket
server {
    listen 443 ssl;
    server_name ws.example.com;

    ssl_certificate /etc/letsencrypt/live/ws.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ws.example.com/privkey.pem;

    location /ws/ {
        proxy_pass http://localhost:8765;
        proxy_http_version 1.1;

        # Critical: these headers enable WebSocket protocol upgrade
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";

        # Standard proxy headers
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Timeout: WebSocket connections can be long-lived
        proxy_read_timeout 86400s;  # 24 hours
    }
}

WebSocket Frame Format

WebSocket frames use a compact binary format with minimal overhead:

Bit 0 (FIN) Bits 1-3 (RSV) Bits 4-7 (Opcode) Bit 8 (MASK) Bits 9-15 (Payload Len) Extended Length (optional) Masking Key (optional) Payload Data
1=final Reserved 0x1=text, 0x2=binary, 0x8=close, 0x9=ping, 0xA=pong 1=masked (client→server) 7-126-127 bytes 16 or 64 bits if len >125 4 bytes if MASK=1 Variable

Clients MUST mask all frames sent to the server (for cache poisoning protection). Servers MUST NOT mask frames sent to clients. This asymmetry is enforced by the protocol.

Subprotocol Negotiation

The client can request a subprotocol during the handshake to specify the message format:

// Client requests a subprotocol
const ws = new WebSocket('wss://api.example.com', ['json-v2', 'json-v1']);
// Server selects the first one it supports: 'json-v2'
# Server selects from requested subprotocols
async def handler(websocket):
    subprotocol = websocket.request_headers.get('Sec-WebSocket-Protocol', '')
    # Server can choose based on client request

Resources

Comments

👍 Was this article helpful?