websockets
websockets is the reference-quality Python implementation of the WebSocket protocol (RFC 6455 plus the RFC 7692 compression extension), built on asyncio. A server is one coroutine per connection: async for message in ws reads, await ws.send() writes, and the library handles the handshake, ping/pong keepalive, backpressure, and clean closes for you. It also ships a threading-based sync API for scripts, a trio integration, and a Sans-I/O protocol core that other servers embed; uvicorn uses it as a WebSocket backend.
The best maintained WebSocket library in Python: 0 open issues and PRs at review time is not a fluke, it reflects an actively tended project. Use it for anything WebSocket-only, learn the new asyncio API, and distrust tutorials written before 2024.
Use it if
- You are building a dedicated WebSocket server or client on asyncio and want strict RFC compliance with backpressure handled correctly
- You need long-lived clients that survive network blips: async for ws in connect(url) reconnects automatically with exponential backoff
- You need a synchronous client for scripts, tests, or notebooks where starting an event loop is overkill (websockets.sync.client)
- You fan out messages to many connections and want the broadcast() helper instead of gathering thousands of send() coroutines
- You want HTTP and WebSocket in one app: HTTP support here is deliberately minimal (enough for a health check), the README itself points you to servers built on top like uvicorn or Sanic; with FastAPI or aiohttp you get both protocols in one process
- Your team is on Python 3.10 or older: version 17 requires Python 3.11+, so older interpreters mean pinning outdated releases and missing fixes
- You need rooms, acknowledgements, or fallback transports: those are Socket.IO semantics, use python-socketio rather than rebuilding them on raw WebSocket
- You will be copy-pasting from old tutorials: the pre-14 legacy API (websockets.serve, handlers taking a path argument) is deprecated but still importable in 17.x, so outdated examples run with warnings and subtly different behavior, which confuses teams mid-migration
Setup reality
pip install websockets pulls prebuilt wheels with a C speedups extension for Linux, macOS, and Windows; no build step, but Python 3.11+ is required. The real friction is the API split: new code must import from websockets.asyncio.server and websockets.asyncio.client, while most StackOverflow answers use the deprecated legacy spelling, and handler signatures differ between them (new handlers take just the connection, legacy ones took connection and path). Behind a proxy or load balancer you will also end up tuning ping_interval and ping_timeout to stop idle disconnects.
Patterns
Minimal asyncio echo serverecho-server
import asyncio
from websockets.asyncio.server import serve
async def echo(websocket):
async for message in websocket:
await websocket.send(message)
async def main():
server = await serve(echo, "localhost", 8765)
await server.serve_forever()
asyncio.run(main())New-style handlers take a single connection argument. If a tutorial shows handler(websocket, path), it targets the deprecated legacy API.
Connect, send, and receive as an asyncio clientasync-client
from websockets.asyncio.client import connect
async def hello():
async with connect("ws://localhost:8765") as websocket:
await websocket.send("Hello")
reply = await websocket.recv()
print(reply)The async context manager closes the connection cleanly on exit, including on exceptions. Use wss:// URLs to get TLS with default certificate verification.
Synchronous client without an event loopsync-client
from websockets.sync.client import connect
with connect("ws://localhost:8765") as websocket:
websocket.send("Hello world!")
message = websocket.recv()
print(message)The sync API runs on threads and is meant for scripts and tests; do not mix it into an asyncio application, use websockets.asyncio there.
Client that reconnects with backoffauto-reconnect-client
from websockets.asyncio.client import connect
from websockets.exceptions import ConnectionClosed
async def listen():
async for websocket in connect("wss://example.com/feed"):
try:
async for message in websocket:
handle(message)
except ConnectionClosed:
continue # reconnects with exponential backoffIterating over connect() retries failed connections and reconnects after drops. Without the except clause, an abnormal close ends the loop instead of reconnecting.
Broadcast a message to every connected clientbroadcast-to-clients
from websockets.asyncio.server import broadcast, serve
CONNECTIONS = set()
async def handler(websocket):
CONNECTIONS.add(websocket)
try:
await websocket.wait_closed()
finally:
CONNECTIONS.remove(websocket)
def notify_all(message):
broadcast(CONNECTIONS, message)broadcast() is a plain function that returns immediately and applies no backpressure; clients whose write buffer is full get skipped rather than slowing everyone down.
Answer a load balancer health check over HTTPhttp-health-check
from http import HTTPStatus
from websockets.asyncio.server import serve
def health_check(connection, request):
if request.path == "/healthz":
return connection.respond(HTTPStatus.OK, "OK\n")
async with serve(handler, "", 8765, process_request=health_check) as server:
await server.serve_forever()Returning a response from process_request short-circuits the WebSocket handshake; returning None lets the upgrade continue. This is the extent of HTTP support, by design.
Tell clean closes from failureshandle-disconnects
from websockets.exceptions import ConnectionClosedError
async def handler(websocket):
try:
async for message in websocket:
await process(message)
except ConnectionClosedError:
log.warning("client dropped without closing handshake")A clean close ends the async for loop with no exception; ConnectionClosedError only fires on abnormal closes. Code that catches broad ConnectionClosed treats normal disconnects as errors.
Exchange JSON messagessend-receive-json
import json
async def handler(websocket):
async for raw in websocket:
event = json.loads(raw)
await websocket.send(json.dumps({"ack": event["id"]}))recv() returns str for text frames and bytes for binary frames; json.loads handles both, but if you require one type, check it or set decode explicitly on recv().
Tune ping/pong keepalive for proxieskeepalive-tuning
from websockets.asyncio.server import serve
server = await serve(
handler, "", 8765,
ping_interval=20, # seconds between pings
ping_timeout=20, # close if no pong within this
)Defaults are 20/20. Idle-timeout proxies (nginx default 60s) kill quiet connections; keep ping_interval below the proxy timeout instead of disabling keepalive with None.
Protect a server with HTTP Basic Authbasic-auth-server
from websockets.asyncio.server import basic_auth, serve
server = await serve(
handler, "", 8765,
process_request=basic_auth(
realm="feed",
credentials=("bot", "s3cret"),
),
)Browsers cannot set Authorization headers on WebSocket connections from JavaScript; Basic Auth works for server-to-server clients, while browser apps usually pass a token in the query string or first message.
Cap message size and queue lengthlimit-message-size
from websockets.asyncio.server import serve
server = await serve(
handler, "", 8765,
max_size=2**20, # 1 MiB per message
max_queue=32, # buffered incoming messages
)max_size protects against memory exhaustion from a single giant frame; oversized messages close the connection with code 1009. Defaults exist but review them before exposing a server publicly.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| aiohttp | PyPI | You need HTTP routes and WebSocket endpoints in the same asyncio server |
| wsproto | PyPI | You want a Sans-I/O WebSocket protocol core to embed in your own I/O layer |
| python-socketio | PyPI | Your clients speak Socket.IO (rooms, acks, reconnection semantics), not raw WebSocket |