WebSockets for Real-Time APIs
Learn WebSockets for real-time API features in FastAPI — from core concepts to hands-on steps, troubleshooting, and what to study next. Master real-time communication for modern APIs.
Focus: websockets for real-time api features
You've built REST APIs that handle requests and responses, but what happens when you need to push data to clients the moment it's available — not when they ask for it? Chat applications, live dashboards, and collaborative tools all demand this, and that's where WebSockets for real-time API features come in. In this lesson, you'll move beyond request-response HTTP and build bidirectional, low-latency communication into your FastAPI applications, giving your users the live experience they expect.
The Problem This Lesson Solves
Traditional REST APIs are like email: you send a request, the server replies, and the connection closes. To get updates, your client has to keep asking — polling — which wastes resources and introduces lag. Imagine a stock ticker that refreshes every second, or a chat app where new messages only appear after you hit refresh. That's the pain WebSockets solve.
Polling works but is inefficient and not truly real-time. Server-Sent Events (SSE) push data one-way, which is great for notifications but not for interactive features. WebSockets establish a persistent, full-duplex connection over a single TCP socket, allowing both client and server to send messages any time. This is the backbone of modern real-time features.
In FastAPI, adding WebSocket support is straightforward thanks to its native integration with Starlette. By the end of this lesson, you'll know how to upgrade an existing API with a real-time endpoint, handle concurrent connections, and debug common pitfalls.
Core Concept: The Persistent Conversation
Think of a WebSocket connection as a phone call versus HTTP's postcards. With postcards (HTTP), each message requires a new envelope, delivery, and reply. With a phone call (WebSocket), you dial once, and once connected, you can talk back and forth freely until someone hangs up.
The WebSocket protocol starts with an HTTP handshake — the client sends a standard GET request with an Upgrade: websocket header. The server responds with a 101 Switching Protocols status, and from then on, the TCP connection is dedicated to the WebSocket frame exchange.
Key terms to know:
- Endpoint: The URL where the WebSocket is available, e.g., /ws.
- Handshake: The initial HTTP-to-WebSocket upgrade.
- Message: A unit of data transferred between client and server, can be text or binary.
- Keep-alive: The connection stays open, and either side can send pings/pongs to verify liveness.
- Close: Either side can end the session with a close frame.
In FastAPI, you define a WebSocket endpoint using @app.websocket("/ws"). The handler receives a WebSocket object, and you control the flow with await websocket.accept(), await websocket.receive_text(), and await websocket.send_text().
How It Works Step by Step
- Client initiates: The browser or client creates a WebSocket connection to the endpoint URL, e.g.,
ws://localhost:8000/ws. - Handshake: FastAPI's websocket handler intercepts the upgrade request and calls
await websocket.accept()to confirm. - Bidirectional messaging: Inside an infinite loop, your server can
receive_text()for incoming messages andsend_text()to push data. Either side can send anytime. - Connection lifecycle: The connection persists until either side closes it. You handle
WebSocketDisconnectexceptions to clean up resources. - Concurrency: Each client gets its own WebSocket instance. To broadcast to all clients, you keep a registry of active connections (e.g., a list or set) and iterate over it.
FastAPI handles the protocol details for you — you focus on your application logic.
Hands-On Walkthrough
Let's build a live chat room — the classic WebSocket use case. We'll create a FastAPI app that accepts WebSocket connections, broadcasts messages to all connected clients, and handles disconnects gracefully.
1. Set up the server
Create a file main.py:
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
class ConnectionManager:
def __init__(self):
self.active_connections: list[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"User says: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast("A user left the chat")
Run with uvicorn main:app --reload.
The ConnectionManager class centralizes connection handling — a pattern you'll reuse in production. The broadcast method iterates over all active sockets, so every message reaches every client.
2. Test with a WebSocket client
You can use the websockets library or a tool like websocat. Here's a Python test script:
import asyncio
import websockets
async def test():
async with websockets.connect("ws://localhost:8000/ws") as ws:
await ws.send("Hello, FastAPI!")
response = await ws.recv()
print(f"Received: {response}")
asyncio.run(test())
Expected output based on the server code:
Received: User says: Hello, FastAPI!
3. Build a minimal browser client
To see the magic, open two browser tabs. Create index.html:
<!DOCTYPE html>
<html>
<head><title>Chat</title></head>
<body>
<input id="msg" placeholder="Type message">
<button onclick="send()">Send</button>
<ul id="log"></ul>
<script>
const ws = new WebSocket("ws://localhost:8000/ws");
ws.onmessage = (event) => {
const li = document.createElement("li");
li.textContent = event.data;
document.getElementById("log").appendChild(li);
};
function send() {
ws.send(document.getElementById("msg").value);
}
</script>
</body>
</html>
Open /index.html in two tabs, type a message in one, and watch it appear in both — that's WebSockets for real-time API features in action.
Compare Options: When to Choose What
WebSockets aren't always the answer. Here's a quick comparison:
| Feature | WebSocket | Server-Sent Events (SSE) | Polling |
|---|---|---|---|
| Direction | Full-duplex (both ways) | Server → Client only | Client → Server (request/unrequest) |
| Latency | Low, immediate | Low, but only pushes | High, depends on interval |
| Complexity | Moderate (stateful) | Low (stateless HTTP) | Very low |
| Use case | Chat, multiplayer games, live collaborative editing | Notifications, news feeds, stock price tickers | Simple status checks, legacy systems |
| Server resource use | Higher (long-lived connections) | Moderate (each client needs a stream) | High (wasted requests) |
| Reconnection | Client must re-handshake | Automatic (EventSource) | Client-controlled |
Choose WebSockets when you need real-time, bidirectional interaction—like a live chat or collaborative whiteboard. Choose SSE when the server only needs to push updates (e.g., a running process log) and you want simpler fallback handling. Polling is for low-traffic cases where real-time isn't critical.
Troubleshooting & Edge Cases
- Handshake fails: If the WebSocket client gets a 403 or connection closed on connect, ensure
websocket.accept()is called before any other logic. FastAPI won't complete the handshake otherwise. - Connection closes unexpectedly: WebSockets drop from network issues or timeouts. Use heartbeats (ping/pong) to detect dead connections and remove them from your registry. In FastAPI, you can
await websocket.receive_text()and catchWebSocketDisconnectto clean up. - Multiple clients, missed messages: If you iterate over
active_connectionswhile a client disconnects, you'll get a runtime error. CatchWebSocketDisconnector use the disconnect method to remove dead sockets before broadcasting. - Broadcasting performance: With hundreds of clients, a simple
forloop works, but consider usingasyncio.gatherto send concurrently (see variation below). - Message too large: By default, FastAPI/Starlette limits WebSocket messages to 1MB. For larger payloads, configure
max_sizeon the endpoint (e.g.,@app.websocket("/ws", max_size=10_000_000)). - Testing: The built-in
TestClientdoesn't handle WebSocket backends directly. Usehttpx.ASGITransportor thewebsocketslibrary in integration tests.
What You Learned & What's Next
You've now added a real-time dimension to your FastAPI toolbox. You understand: - The problem of real-time data delivery and why WebSockets solve it. - The mental model of a persistent, bidirectional connection. - How to implement a WebSocket endpoint with FastAPI, including connection management and broadcasting. - How to choose between WebSockets, SSE, and polling. - Common pitfalls and how to sidestep them.
This lesson directly supports the learning objectives: you can explain the core idea behind WebSockets for real-time API features, and you completed a practical exercise implementing a chat room.
Next in the track, you'll explore Authentication for WebSockets — how to verify tokens during the handshake, secure the connection, and prevent unauthorized access to your real-time endpoints. That's a critical skill for production deployments.
Practice recap
Build a small 'live notification' system: create a WebSocket endpoint that pushes a message every 2 seconds. Write a Python client using the websockets library to connect and print 5 messages, then close. This reinforces connection lifecycle, message handling, and auto-closing logic — then you're ready to move on to authentication for WebSockets.
Common mistakes
- Forgetting to call
await websocket.accept()before any other send/receive — this fails the handshake and the client disconnects. - Not catching
WebSocketDisconnectand cleaning up the connection list, leading to attempts to send to dead sockets and runtime errors. - Blocking the event loop with synchronous I/O (like a
requestscall) inside a WebSocket handler — always useawaitor run blocking code in a thread pool. - Ignoring security: no authentication or origin checks on the WebSocket endpoint, allowing any site to connect and abuse your API.
Variations
- Use
asyncio.gatherto broadcast messages to all clients concurrently instead of a sequential loop, improving performance with many connections. - Add a heartbeat mechanism: send a ping frame periodically (or rely on protocol-level pings) and close connections that don't respond, to free up resources.
- For one-way updates, prefer Server-Sent Events (SSE) over WebSockets — simpler, automatic reconnection, and works over regular HTTP.
Real-world use cases
- Live chat application (e.g., Slack, Discord) with real-time message delivery and presence indicators.
- Real-time dashboard streaming metrics (e.g., server CPU load, stock prices) straight from the backend to the browser without page refreshes.
- Collaborative editor (Google Docs style) where keystrokes and cursor positions sync across users instantly via WebSocket messages.
Key takeaways
- WebSockets provide full-duplex, low-latency communication over a single persistent connection, unlike request-response HTTP.
- FastAPI's
@app.websocketdecorator lets you upgrade an endpoint to handle WebSocket connections cleanly. - You must call
await websocket.accept()before receiving or sending data — the handshake requires it. - Manage active connections in a registry and handle
WebSocketDisconnectto prevent leaks and errors. - Choose WebSockets for bidirectional real-time; use SSE for one-way pushes, and polling only as a last resort for simplicity.
- Always secure WebSocket endpoints with authentication and origin checks to prevent abuse.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.