Using Python’s websockets Library: Building Bidirectional Real-Time Applications

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Quick start: Set up a bidirectional connection in 5 minutes

Install the library via pip:

pip install websockets

Create a server.py file to receive and respond instantly to client messages:

# server.py
import asyncio
import websockets

async def echo(websocket):
    async for message in websocket:
        print(f"[Server received]: {message}")
        await websocket.send(f"Server response: {message}")

async def main():
    async with websockets.serve(echo, "localhost", 8765):
        print("Server is listening on ws://localhost:8765")
        await asyncio.Future()  # Keep process running in background

if __name__ == "__main__":
    asyncio.run(main())

Next, create client.py to send test data:

# client.py
import asyncio
import websockets

async def send_message():
    uri = "ws://localhost:8765"
    async with websockets.connect(uri) as websocket:
        await websocket.send("Ping from Client!")
        response = await websocket.recv()
        print(f"[Client received]: {response}")

if __name__ == "__main__":
    asyncio.run(send_message())

Open two terminal windows. Run python server.py first, then run python client.py. The output will be displayed almost instantly with sub-5ms latency.

HTTP Polling vs WebSocket: When Should You Switch?

Traditional HTTP operates on a stateless Request-Response model. To receive continuous state updates, clients must rely on polling—sending requests periodically every 1–3 seconds.

This approach reveals significant drawbacks under high loads:

  • HTTP Overhead: Each request carries 500–1,000 bytes of HTTP headers (Cookie, User-Agent, Auth). In contrast, WebSocket only requires 2–6 bytes of framing overhead per message once the handshake is complete.
  • Server Resources: 10,000 users polling every second generates 10,000 requests/second on the server. With WebSocket, you simply maintain 10,000 open TCP connections and transfer data only when events occur.
  • Latency: Polling introduces latency tied to the polling interval. WebSocket pushes data to clients the exact moment an event occurs.

Asyncio Under the Hood in the websockets Library

The library runs entirely on the asyncio event loop. While one connection awaits network I/O, the CPU immediately switches to handle packets from other connections. Thanks to non-blocking I/O, a single-threaded Python worker can maintain tens of thousands of concurrent sockets with minimal memory overhead.

Advanced: Building a Multi-User Broadcast Room

A common use case is pushing data simultaneously to multiple subscribers, such as real-time stock tickers or chat rooms.

The library includes a built-in websockets.broadcast helper, optimized to push messages to all sockets in a set:

# chat_server.py
import asyncio
import websockets

CONNECTED_CLIENTS = set()

async def handler(websocket):
    CONNECTED_CLIENTS.add(websocket)
    client_ip = websocket.remote_address
    print(f"+ Client connected: {client_ip} (Online: {len(CONNECTED_CLIENTS)})")
    
    try:
        async for message in websocket:
            # Broadcast to all active clients
            websockets.broadcast(CONNECTED_CLIENTS, f"{client_ip[0]}: {message}")
    except websockets.exceptions.ConnectionClosed:
        pass
    finally:
        CONNECTED_CLIENTS.remove(websocket)
        print(f"- Client disconnected: {client_ip} (Online: {len(CONNECTED_CLIENTS)})")

async def main():
    async with websockets.serve(handler, "0.0.0.0", 8765):
        print("Broadcast Server running on port 8765...")
        await asyncio.Future()

if __name__ == "__main__":
    asyncio.run(main())

3 Essential Tips for Production Deployment

1. Maintain Connections Through Load Balancers (Heartbeat / Ping-Pong)

AWS ALB or Cloudflare often drop idle TCP connections after 60 seconds. Configure periodic pings to keep connections alive:

# Send ping every 20s, drop connection if no pong response within 10s
async with websockets.serve(
    handler, 
    "0.0.0.0", 
    8765, 
    ping_interval=20, 
    ping_timeout=10
):
    await asyncio.Future()

2. Client-Side Auto-Reconnect (Exponential Backoff)

Mobile and Wi-Fi networks are prone to intermittent dropouts. Clients should catch exceptions and retry using exponential backoff to avoid overwhelming the server:

# robust_client.py
import asyncio
import websockets

async def connect_with_retry(uri):
    retry_delay = 1
    max_delay = 30
    
    while True:
        try:
            async with websockets.connect(uri) as websocket:
                print("WebSocket connection established!")
                retry_delay = 1  # Reset delay after stable connection
                async for msg in websocket:
                    print(f"Data: {msg}")
        except (websockets.exceptions.ConnectionClosed, OSError) as e:
            print(f"Connection lost ({e}). Retrying in {retry_delay}s...")
            await asyncio.sleep(retry_delay)
            retry_delay = min(retry_delay * 2, max_delay)

3. Reverse Proxy with Nginx and SSL (WSS)

Avoid exposing Python ports directly to the internet. Instead, place your server behind Nginx to handle SSL termination and proxy requests:

location /ws/ {
    proxy_pass http://127.0.0.1:8765;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 300s;
}

Additionally, validate JWT tokens in query parameters or headers during the initial handshake to reject unauthorized requests before allocating socket resources.

Share: