How to Create a Python WebSocket API: Practical Example
When you need real‑time communication between a client and a server, WebSocket often feels like the magic ingredient that turns a static app into an interactive experience. Python, with its rich ecosystem, makes wiring up a WebSocket API surprisingly approachable. Below is a step‑by‑step walk‑through that shows not just the code, but the thinking behind each part.
Why Choose WebSocket Over Traditional HTTP?
HTTP follows a request‑response pattern: the client asks, the server answers, and the connection usually closes. That works fine for loading pages, but it falls short when you want live updates—think chat messages, stock tickers, or multiplayer game states.
WebSocket keeps the TCP connection open, allowing both sides to push data whenever they need to. The result is lower latency and less overhead, because you avoid the constant round‑trip of new HTTP requests.
Preparing the Environment
First, make sure you have Python 3.8+ installed. The example relies on FastAPI for the web framework and uvicorn as the ASGI server. Both are lightweight and well‑documented.
pip install fastapi uvicorn websockets
Optional, but recommended: set up a virtual environment to keep dependencies tidy.
Setting Up the Basic FastAPI App
Below is the skeleton of a FastAPI application that will host our WebSocket endpoint.
from fastapi import FastAPI, WebSocketapp = FastAPI()
@app.get("/")
async def root():
return {"message": "Welcome to the WebSocket demo"}
This route simply confirms that the server is running. The real work starts with the WebSocket route.
Creating the WebSocket Endpoint
We’ll add a new path /ws that accepts WebSocket connections. The function receives a WebSocket object, which we must first accept before exchanging messages.
@app.websocket("/ws")async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
data = await websocket.receive_text()
# Echo the received message back to the client
await websocket.send_text(f"Server received: {data}")
except Exception as e:
await websocket.close()
A few subtle points:
- Accepting the connection is mandatory; otherwise the client will time out.
- The
while Trueloop keeps the channel open, listening for incoming text frames. - Any exception (like a client disconnect) gracefully closes the socket.
Adding Simple Business Logic
You probably don’t want a raw echo service. Let’s pretend we’re building a tiny chat room. A shared list will hold all active connections, and each incoming message gets broadcast to every participant.
connected_clients = set()@app.websocket("/chat")
async def chat_endpoint(websocket: WebSocket):
await websocket.accept()
connected_clients.add(websocket)
try:
while True:
data = await websocket.receive_text()
for client in connected_clients:
if client != websocket:
await client.send_text(data)
finally:
connected_clients.remove(websocket)
await websocket.close()
Notice the finally block—no matter how the loop exits, we clean up the client set to avoid memory leaks.
Testing the API Locally
Run the server with:
uvicorn myapp:app --reloadOpen two browser tabs pointing to a simple HTML page that establishes a WebSocket connection to ws://localhost:8000/chat. When you type a message in one tab, it appears in the other—proof that the broadcast works.
Deploying to Production
While uvicorn with --reload is perfect for development, a production setup usually pairs it with a process manager like Gunicorn and an ASGI worker class.
gunicorn -k uvicorn.workers.UvicornWorker myapp:appDon’t forget to secure the endpoint with TLS (HTTPS/WSS). Most cloud providers let you terminate TLS at a load balancer, then forward plain WebSocket traffic to your app.
Common Pitfalls and How to Avoid Them
- Blocking code: Avoid long‑running synchronous functions inside the WebSocket loop; they will block all connections. Use
asyncioutilities or run heavy tasks in a background worker. - Uncaught exceptions: An unexpected error will break the loop and drop the client. Wrap receive/send calls in
try/exceptblocks, and consider logging the error for later debugging. - Resource leaks: Forgetting to remove a client from the shared set can lead to stale references and memory growth over time.
Extending the Example
From here you can branch out in many directions:
- Integrate authentication tokens and reject unauthorized connections.
- Persist messages in a database so new participants see recent chat history.
- Use
JSONpayloads instead of plain text to carry richer data structures. - Scale out with a message broker like Redis Pub/Sub when you need dozens of server instances.
The core pattern—accept, loop, broadcast, clean up—remains the same, no matter how sophisticated the surrounding features become.