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.
| Feature | HTTP (Request-Response) | WebSocket (Full-Duplex) |
|---|---|---|
| Model | Client → Request → Server Client ← Response ← Server | Client ↔ Server (Simultaneous two-way) |
| Connection | Closed after the response finishes | Stays open (persistent) |
| Overhead | High (HTTP headers ~200-800 bytes per request) | Low (frame header 2-14 bytes per message) |
| Best fit | REST APIs, form submissions, file downloads, SSR | Chat, 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 ClosedThe 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())
Avoidasyncio.get_event_loop().run_until_complete(), which appears in many old examples — this API has been deprecated since Python 3.10. Useasyncio.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()notget_event_loop()— the old API has been deprecated since Python 3.10.- Handle
ConnectionClosedErrorandConnectionClosedOK— both need handling so the server doesn’t crash when a client disconnects.- Send data in JSON format with a
typefield — 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_intervalandping_timeoutso 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.