Real-Time Communication with websockets
HTTP is a request-and-response protocol. The client asks, the server answers, and the conversation ends there. That model is fine for fetching pages, but it falls apart when you need the server to push updates: chat messages, live dashboards, multiplayer game state, IoT telemetry. Polling works but burns bandwidth and adds latency. WebSockets fix this by upgrading a single TCP connection into a full-duplex channel where either side can send messages at any time, which is the foundation of real-time communication in modern apps.
The websockets library is the standard way to do this in modern Python. It is asyncio-first, BSD-3 licensed, requires Python 3.10 or newer, and ships with a C extension for fast UTF-8 validation. The current stable release is 16.0. This guide walks through the patterns you will actually use: an echo server, a client, broadcasting to many clients, ping/pong keepalive, and the three errors that trip up almost everyone.
From Python, you can check the version programmatically, which is handy for build scripts and health checks:
python -c "import websockets; print(websockets.__version__)"
# output: 16.0
TL;DR
- Install with
pip install websockets. The package is third-party, not in the standard library, andasynciodoes not include a WebSocket implementation. - A server is
serve(handler, host, port)fromwebsockets.asyncio.server, used as an async context manager. The handler runs once per connection. - A client is
connect(uri)fromwebsockets.asyncio.client, also an async context manager. - Always catch
ConnectionClosedaroundrecv()andsend()loops. Abnormal closes are normal in production and should not crash your handler. - For fan-out, use
broadcast()fromwebsockets.asyncio.serverinstead of looping and awaitingws.send()per client. The naive pattern lets one slow consumer block everyone behind it.
What are WebSockets and when should you reach for them?
WebSockets are an IETF standard (RFC 6455) that runs over a single TCP connection and lets either side send messages at any time, with very little per-message overhead. They are a good fit for chat and messaging apps, live dashboards, monitoring and telemetry, collaborative editing, and multiplayer games, basically anything where the server needs to push without being asked.
They are a bad fit for one-shot RPC (use httpx or requests), pub/sub at scale (use Redis, NATS, or Kafka), or push-only needs (use Server-Sent Events or webhooks). The websockets docs intro frames the same tradeoffs in more detail if you want the official angle.
How do I install the websockets library?
The package is third-party. It is not part of the standard library, and Python’s own asyncio module has no WebSocket implementation. Install it with pip:
pip install websockets
Confirm the install by checking the version, and make sure the console script ended up on your PATH so websockets --version is callable from a shell:
websockets --version
# output: 16.0
If you see an older version, you are probably looking at answers about websocket-client (note the hyphen), which is a different, sync-only library. The two share a name and have been confused in Stack Overflow answers for years. Use websockets for anything new. The full package page is on PyPI and the canonical reference lives in the websockets documentation.
Your first WebSocket server
The canonical starter is an echo server that sends back whatever it receives. Save this as echo_server.py:
#!/usr/bin/env python
"""echo_server.py — Python 3.10+, websockets >= 13."""
import asyncio
from websockets.asyncio.server import serve
async def echo(websocket):
async for message in websocket:
await websocket.send(message)
async def main():
async with serve(echo, "localhost", 8765) as server:
await server.serve_forever()
asyncio.run(main())
Three things are worth noticing. First, serve() returns an async context manager, so async with takes care of startup and shutdown. Second, the handler runs once per connection, and the async for loop exits cleanly when the client disconnects. Third, serve_forever() is awaitable and blocks until the server is closed, which makes it pair nicely with Ctrl+C for clean shutdowns.
The serve() function takes a long list of options. The ones you will tune most often:
origins— a list of allowed origins. Set this in production to defend against Cross-Site WebSocket Hijacking.ping_interval=20,ping_timeout=20— the heartbeat. Every 20 seconds the server sends a ping; if no pong arrives within 20 seconds, the connection is closed.max_size=1048576— 1 MiB max frame size. Bump this up for binary streams, lower it to defend against DoS.compression="deflate"— on by default. Set to"disable"for high-throughput binary traffic where CPU matters more than bandwidth.
For the full parameter list (and what each one does to the upgrade handshake), see the server API reference.
How do I connect from Python?
A matching async client (echo_client.py):
#!/usr/bin/env python
"""echo_client.py"""
import asyncio
from websockets.asyncio.client import connect
async def hello():
async with connect("ws://localhost:8765") as websocket:
await websocket.send("Hello world!")
reply = await websocket.recv()
print(f"Server said: {reply}")
asyncio.run(hello())
Run the server in one terminal, then the client in another. The client opens the connection, sends a single message, prints whatever comes back, and exits:
python echo_client.py
# output: Server said: Hello world!
connect() also takes a ping_interval and ping_timeout, so you can tune the same keepalive behavior on the client side. For reconnect-with-backoff logic, use the async for websocket in connect(...) iterator pattern from the client API reference. It transparently reconnects and resumes on ConnectionClosed, which is what you want in long-lived consumers.
For scripts that cannot go fully async, the library ships a sync API. No await keywords, no event loop:
"""threading_client.py — drop-in for non-async code."""
from websockets.sync.client import connect
with connect("ws://localhost:8765") as websocket:
websocket.send("Hello world!")
print(f"Server said: {websocket.recv()}")
The sync API is a thin wrapper that runs the event loop on a background thread. It is great for quick scripts and embedding into legacy code, but do not use it inside an already-running asyncio program; the two event loops will fight. For blocking I/O inside an async handler, the right tool is asyncio.to_thread() or a worker pool, not the sync websockets API.
Can a Python WebSocket server talk to a browser?
Yes, with no extra code. A Python websockets server speaks plain RFC 6455, so any browser WebSocket client works out of the box. No JavaScript library needed:
<script>
const ws = new WebSocket("ws://localhost:8765");
ws.onmessage = (ev) => console.log("got:", ev.data);
ws.onopen = () => ws.send("hi from browser");
</script>
Open the browser dev tools, switch to the Network tab, and filter on “WS”. You will see frames flying in both directions, each with a timestamp and payload size. That view is invaluable when you need to figure out whether a hang-up is on the client or the server side. The browser-side API is documented on MDN’s WebSocket page if you want to wire up reconnect logic, subprotocols, or binary frames on the JS side.
How do I send structured data over a WebSocket?
WebSocket frames are just str (text) or bytes (binary). To send structured data, serialize it. JSON is the obvious choice:
import json
from websockets.asyncio.client import connect
async with connect("wss://api.example.com/feed") as ws:
await ws.send(json.dumps({"action": "subscribe", "channel": "trades"}))
async for raw in ws:
data = json.loads(raw)
print(data["price"], data["size"])
Wrap json.loads() in a try/except. A peer that sends malformed JSON will otherwise kill your handler with json.JSONDecodeError. The library itself will not protect you from bad payloads within a frame.
For a stricter schema, msgspec or pydantic are good companions: define a model, validate, and raise a domain-specific error you can catch at the edge of your handler instead of letting a JSONDecodeError escape.
How do I broadcast a message to many clients?
The interesting case is fan-out: one event in, hundreds of clients notified out. The naive approach is wrong:
# DON'T do this
for ws in CLIENTS:
await ws.send(message) # one slow client blocks everyone behind it
A sequential loop is the most common source of “some clients see the message, others time out” bugs. Use the built-in broadcast() helper from websockets.asyncio.server, which sends concurrently and silently drops dead connections. Here is a small chat-room server that ticks once per second to every connected client, useful as a starting point for live dashboards:
"""chat_server.py — every connected client sees every message."""
import asyncio
from websockets.asyncio.server import serve
from websockets.asyncio.server import broadcast
from websockets.exceptions import ConnectionClosed
CLIENTS: set = set()
async def handler(websocket):
CLIENTS.add(websocket)
try:
async for _ in websocket: # we just drain; clients receive broadcasts
pass
finally:
CLIENTS.discard(websocket)
async def publisher():
i = 0
while True:
await asyncio.sleep(1)
i += 1
broadcast(CLIENTS, f"tick {i}")
async def main():
async with serve(handler, "localhost", 8765):
await publisher()
asyncio.run(main())
Two subtleties. First, the handler’s try/finally ensures a client is removed from the set even when its connection dies mid-loop. Second, broadcast() returns immediately; if you need delivery confirmation, roll your own with asyncio.gather(*[ws.send(m) for ws in CLIENTS], return_exceptions=True).
broadcast() is still O(N) in number of connections. Past roughly 10,000 concurrent clients you will want Redis pub/sub, NATS, or a dedicated broker to do the fan-out, with each websockets process subscribing to a slice of the clients. The broadcasting guide walks through the tradeoffs in more depth, including the recommended pattern for iterating a copy of the connection set.
How does keepalive work?
A TCP connection that sits idle for two minutes will often get killed by a NAT box, a corporate proxy, or a load balancer. WebSockets solve this with ping/pong frames. The server sends a ping every ping_interval seconds; if the client does not respond with a pong within ping_timeout seconds, the connection is closed and ConnectionClosedError is raised inside any active recv().
The defaults (ping_interval=20, ping_timeout=20) give you about 40 seconds of latency tolerance. That is enough for most deployments. If you have high-latency clients (mobile networks, satellite links), raise ping_timeout to 60 or 90.
If you see this error in production:
ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received
the peer was too slow to respond to a ping. The fix is either to raise ping_timeout or to find the slow path on the server side. CPU saturation, blocking calls inside a handler, and GC pauses are the usual suspects.
What do common WebSocket errors mean?
| Error | Cause | Fix |
|---|---|---|
ConnectionClosedError: no close frame received or sent | TCP died (Wi-Fi drop, laptop sleep, proxy timeout) | Catch ConnectionClosed in the handler; raise proxy_read_timeout on nginx if behind a load balancer |
ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout | Peer too slow to respond to ping | Raise ping_timeout, or profile the server for blocking work |
RuntimeError: set changed size during iteration | Broadcast loop modified CLIENTS during iteration | Iterate CLIENTS.copy() |
PayloadTooBig | Frame larger than max_size (1 MiB default) | Raise max_size for binary streams |
InvalidStatusCode from connect() | Handshake returned non-101 (404, 502, auth failure) | Check the URL, auth headers, and that the server is actually a WebSocket endpoint |
Always wrap the message loop in a try block so an abnormal close is logged, not raised out of the handler:
from websockets.exceptions import ConnectionClosed
async def handler(websocket):
try:
async for message in websocket:
await websocket.send(message)
except ConnectionClosed:
pass # peer went away; nothing to do
The single biggest source of “all my WebSocket clients time out” reports is blocking the event loop inside a handler. Never call time.sleep, requests.get, or open(...).read() inside the handler. Use asyncio.sleep, httpx, and aiofiles instead. See the asyncio basics guide and the async/await patterns guide for the non-blocking equivalents of common operations. The official common errors page catalogs the rest.
When is websockets the wrong tool for real-time communication?
The websockets library is intentionally narrow. It is a WebSocket server, not an HTTP framework. The moment you need GET /users and WS /chat in the same process, graduate to FastAPI, which embeds the same websockets library under uvicorn.
One escape hatch: you can add a single HTTP endpoint (for a load balancer health check, say) without leaving the library, using process_request:
async def health_check(connection, request):
if request.path == "/health":
return connection.respond(200, "OK\n")
# return None to continue the WebSocket handshake
async with serve(handler, "localhost", 8765, process_request=health_check):
...
This is the recommended way to add a minimal HTTP route to a websockets server. For anything beyond a health check, though, you want uvicorn.
If you also need an async HTTP client to call third-party APIs from inside a handler, reach for aiohttp or httpx. If you need pub/sub across multiple processes, look at Redis or Celery. For the raw socket layer underneath all of this, the socket module reference covers the TCP plumbing.
And if you do not actually need bidirectional messaging — if you only need the server to push to the client — Server-Sent Events or even a webhook receiver are often a better fit than WebSockets. See the webhooks tutorial for that pattern. The real-time communication space is broad, and the right tool depends on traffic shape, directionality, and scale rather than novelty.
Frequently asked questions
What Python version do I need?
websockets 16.0 requires Python 3.10 or newer. If you are stuck on 3.9, the last release that supported it was websockets 12.x. Anything older than 3.8 will not work at all on recent versions.
What is the difference between websockets and websocket-client?
websockets (no hyphen) is the modern, asyncio-first library this guide covers. websocket-client (with a hyphen) is an older, sync-only library. They share a name and have been confused in old Stack Overflow answers. Use websockets for new code.
How do I keep WebSocket connections alive across flaky networks?
Tune ping_interval and ping_timeout on both serve() and connect(). For consumers, use the async for websocket in connect(...) iterator pattern, which transparently reconnects on ConnectionClosed:
from websockets.asyncio.client import connect
from websockets.exceptions import ConnectionClosed
async for websocket in connect("wss://example.com/feed"):
try:
async for message in websocket:
handle(message)
except ConnectionClosed:
continue # reconnect with the same parameters
This is the cleanest way to write a daemon that survives transient network blips.
Can I use websockets with FastAPI?
Yes, but FastAPI has its own WebSocket support built on top of websockets. If you only need WebSockets (no other HTTP routes), the bare library is lighter. If you need both HTTP and WS in one process, use FastAPI.
How many concurrent clients can one process handle?
For chat-style workloads (small messages, periodic keepalive), tens of thousands per process on a modern server. The limit is usually per-connection memory and the file descriptor count, not CPU. Beyond that, fan out across multiple processes behind a load balancer.
Is the websockets library production-ready?
Yes. It is BSD-3 licensed, has a C extension for fast UTF-8 validation, and powers a lot of infrastructure. The main thing to add in production is origin checking (origins=), structured logging, and metrics around connection counts and message rates.