WebSockets #

HTTP communication is one-way: the client sends a request, the server replies, the connection closes. For applications that need real-time updates — chat, live notifications, stock price dashboards, or multiplayer games — this pattern is inefficient because the client has to keep sending new requests just to check whether new data exists. WebSockets solve this by building a persistent two-way connection between client and server: once the connection is open, both sides can send messages to each other at any time without the overhead of new requests.

WebSocket vs HTTP #

Before getting into the implementation, it’s important to understand the fundamental differences between the two.

FeatureHTTP (Request-Response)WebSocket (Full-Duplex)
ModelClient → Request → Server Client ← Response ← ServerClient ↔ Server (Simultaneous two-way)
ConnectionClosed after the response finishesStays open (persistent)
OverheadHigh (HTTP headers ~200-800 bytes per request)Low (frame header 2-14 bytes per message)
Best fitREST APIs, form submissions, file downloads, SSRChat, live feeds, online gaming, real-time dashboards

To visualize how the HTTP Upgrade handshake kicks off a WebSocket connection and how data is sent in both directions (full-duplex), look at the sequence diagram below:

sequenceDiagram
    participant K as "Client (Browser)"
    participant S as "Server (Python websockets)"
    Note over K, S: 1. HTTP Handshake (Upgrade)
    K->>S: GET /ws HTTP/1.1 (Upgrade: websocket)
    S-->>K: HTTP/1.1 101 Switching Protocols
    Note over K, S: 2. Connection Open (Full-Duplex TCP)
    K->>S: Frame Message (e.g. "Hello Server")
    S->>K: Frame Message (e.g. "Hello Client")
    Note over K, S: 3. Closing the Connection
    K->>S: Close Frame
    S-->>K: Close Acknowledgment
    Note over K, S: Connection Closed

The Handshake Process #

A WebSocket starts as a regular HTTP request, then gets upgraded to the WebSocket protocol:

Client → Server:
  GET /chat HTTP/1.1
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

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

After this: the TCP connection stays open,
the protocol switches from HTTP to WebSocket frames.
Because WebSocket starts from HTTP, it’s compatible with existing web infrastructure — firewalls, load balancers, and reverse proxies (like Nginx) generally support WebSocket with minimal configuration.

Installation #

pip install websockets

The websockets library is based on asyncio — all handlers are written as async coroutines. This lets a single server handle thousands of connections concurrently with high efficiency.


Basic WebSocket Server #

The simplest server: an echo server that bounces back every message it receives.

import asyncio
import websockets

async def handler(websocket):
    """The handler is called once per client connection."""
    print(f"Client connected: {websocket.remote_address}")
    try:
        async for message in websocket:
            print(f"Received: {message}")
            await websocket.send(f"Echo: {message}")
    except websockets.ConnectionClosedError as e:
        print(f"Connection forcibly closed: {e}")
    except websockets.ConnectionClosedOK:
        print("Client closed the connection normally.")
    finally:
        print(f"Client {websocket.remote_address} left.")

async def main():
    # serve() handles the accept loop automatically
    async with websockets.serve(handler, "localhost", 8765):
        print("WebSocket server running at ws://localhost:8765")
        await asyncio.Future()  # run forever

if __name__ == "__main__":
    asyncio.run(main())
Avoid asyncio.get_event_loop().run_until_complete(), which appears in many old examples — this API has been deprecated since Python 3.10. Use asyncio.run() as the main entry point of an async program.

WebSocket Client #

import asyncio
import websockets

async def client():
    uri = "ws://localhost:8765"
    async with websockets.connect(uri) as ws:
        message_list = ["Hello!", "How are you?", "Goodbye!"]
        for message in message_list:
            await ws.send(message)
            response = await ws.recv()
            print(f"Server: {response}")

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

Sending and Receiving JSON #

Almost every real WebSocket application exchanges structured data, not plain strings. The common convention is wrapping the payload in JSON with a type field as the message kind marker.

import asyncio
import json
import websockets

async def handler(websocket):
    async for raw_message in websocket:
        try:
            message = json.loads(raw_message)
        except json.JSONDecodeError:
            await websocket.send(json.dumps({
                "type": "error",
                "message": "Invalid message format — must be JSON."
            }))
            continue

        msg_type = message.get("type")

        if msg_type == "ping":
            await websocket.send(json.dumps({"type": "pong"}))

        elif msg_type == "echo":
            await websocket.send(json.dumps({
                "type": "echo",
                "data": message.get("data")
            }))

        else:
            await websocket.send(json.dumps({
                "type": "error",
                "message": f"Unknown message type: {msg_type}"
            }))

async def main():
    async with websockets.serve(handler, "localhost", 8765):
        await asyncio.Future()

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

A client sending JSON:

import asyncio
import json
import websockets

async def client():
    async with websockets.connect("ws://localhost:8765") as ws:
        # send a ping
        await ws.send(json.dumps({"type": "ping"}))
        response = json.loads(await ws.recv())
        print(f"Ping response: {response}")  # {'type': 'pong'}

        # send an echo
        await ws.send(json.dumps({"type": "echo", "data": {"value": 42}}))
        response = json.loads(await ws.recv())
        print(f"Echo response: {response}")  # {'type': 'echo', 'data': {'value': 42}}

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

Broadcast Server — Chat Room #

The most common WebSocket use case is broadcasting: one message from one client is sent to all connected clients. This is the foundation of chat apps, live notifications, or real-time price updates.

import asyncio
import json
import websockets

# A set to track all active connections
active_clients: set[websockets.WebSocketServerProtocol] = set()

async def broadcast(message: str):
    """Send a message to all connected clients."""
    if not active_clients:
        return
    # websockets.broadcast() is more efficient than a manual loop
    websockets.broadcast(active_clients, message)

async def handler(websocket):
    # Register the new client
    active_clients.add(websocket)
    name = f"User-{len(active_clients)}"
    print(f"[+] {name} joined | Total: {len(active_clients)}")

    await broadcast(json.dumps({
        "type": "system",
        "message": f"{name} joined the chat room."
    }))

    try:
        async for raw_message in websocket:
            try:
                data = json.loads(raw_message)
                text = data.get("text", "")
            except json.JSONDecodeError:
                text = raw_message

            print(f"[{name}]: {text}")
            await broadcast(json.dumps({
                "type": "chat",
                "from": name,
                "text": text
            }))

    except websockets.ConnectionClosedError:
        pass
    finally:
        # Remove the client from the list on disconnect
        active_clients.discard(websocket)
        print(f"[-] {name} left | Remaining: {len(active_clients)}")
        await broadcast(json.dumps({
            "type": "system",
            "message": f"{name} left the chat room."
        }))

async def main():
    async with websockets.serve(handler, "localhost", 8765):
        print("Chat server running at ws://localhost:8765")
        await asyncio.Future()

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

Keepalive with Ping/Pong #

Idle WebSocket connections can be dropped by firewalls, load balancers, or proxies that assume the connection is dead. The ping/pong mechanism keeps the connection alive.

The websockets library handles ping/pong automatically — you just configure the interval:

async def main():
    async with websockets.serve(
        handler,
        "localhost",
        8765,
        ping_interval=20,   # send a ping every 20 seconds
        ping_timeout=10,    # wait at most 10 seconds for a pong
                            # if no pong arrives, the connection is considered dead
    ):
        await asyncio.Future()

To set an overall connection timeout:

async def client_with_timeout():
    async with websockets.connect(
        "ws://localhost:8765",
        ping_interval=20,
        ping_timeout=10,
        open_timeout=5,     # timeout when opening the connection
    ) as ws:
        await ws.send("Hello!")
        response = await asyncio.wait_for(ws.recv(), timeout=5.0)
        print(response)

When to Use WebSocket vs Alternatives #

Use WebSocket when:
  ✓ The server needs to push data to clients without being asked (notifications, chat)
  ✓ Two-way communication with low latency (gaming, collaboration)
  ✓ Many frequent small updates (stock prices, IoT sensors)

Consider Server-Sent Events (SSE) when:
  ✗ You only need server → client (one-way), not two-way
  ✗ You want a simpler solution based on plain HTTP
  ✗ Examples: news feeds, progress bars, log streaming

Consider plain HTTP polling when:
  ✗ Updates aren't that frequent (every few minutes)
  ✗ Strict real-time isn't needed
  ✗ The infrastructure doesn't support persistent connections

Summary #

  • WebSocket is a persistent two-way connection — after the initial HTTP handshake, client and server can send messages to each other at any time without the overhead of new requests.
  • Use asyncio.run() not get_event_loop() — the old API has been deprecated since Python 3.10.
  • Handle ConnectionClosedError and ConnectionClosedOK — both need handling so the server doesn’t crash when a client disconnects.
  • Send data in JSON format with a type field — this convention makes message routing easy on both the server and client side.
  • For broadcasting, use websockets.broadcast() — more efficient than sending one by one in a loop.
  • Enable ping/pong with ping_interval and ping_timeout so idle connections aren’t dropped by firewalls or proxies.
  • WebSocket isn’t a replacement for HTTP — use REST APIs for regular CRUD operations; WebSocket for real-time communication that genuinely needs server push.

← Previous: Sockets   Next: Web Servers →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact