Search Topics
Search across all FastAPI topics
WebSockets
Real-TimeYou built a chat app. Two users connected. Messages went nowhere. Let's fix that.
You build a chat app with WebSockets. Two users connect. User A sends a message. User B never receives it. You check the code — each WebSocket connection is independent. There's no built-in broadcast.
# Your WebSocket endpoint:
@app.websocket("/ws")
async def chat(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"You said: {data}")
# ← Only echoes back to the SAME user!
# User B never receives User A's messages.
# User A sends: "Hello everyone!"
# User A sees: "You said: Hello everyone!"
# User B sees: ... nothing.Question
Why doesn't User B get the message? Because WebSocket connections are completely isolated. Each client talks to the server through its own private tunnel. If you want messages to reach other clients, you need to build that yourself.
WebSocket vs HTTP: Two Different Worlds
HTTP is like sending letters — you write one, send it, get a reply, done. WebSockets are like a phone call — you connect once and both sides can talk whenever they want.
HTTP (request-response):
Client sends request
GET /messages
Server responds
Here's your data
Connection closes
Done. Start over for next request.
WebSocket (persistent connection):
HTTP upgrade
Switch to WebSocket
Connection open
Both sides can talk anytime
Messages flow
Client ↔ Server
Until close
Either side can end it
websocket basics
What you just learned
HTTP connections are one-shot: request, response, close
WebSocket connections stay open — either side can send messages at any time
Each WebSocket connection is independent — there's no built-in way to talk between connections
A Basic WebSocket: Echo Server
Let's start with the simplest possible WebSocket — it accepts a connection, listens for messages, and echoes them back. This is exactly the "broken" chat from the hook, but it's a perfect starting point.
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept() # Step 1: accept the connection
while True:
# Step 2: wait for a message
data = await ws.receive_text()
# Step 3: send something back
await ws.send_text(f"You said: {data}")
# This loops forever until the client disconnectsWhen Users Disappear
Clients disconnect all the time — they close the tab, lose WiFi, or their battery dies. Without handling this, your server throws an unhandled exception.
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
try:
while True:
data = await ws.receive_text()
await ws.send_text(f"Echo: {data}")
except WebSocketDisconnect:
# Client left — clean up gracefully
print("Client disconnected")Watch out
Always wrap your WebSocket loop in try/except. Without it, every disconnection becomes an unhandled exception in your logs. Noisy and misleading.
websocket lifecycle
What you just learned
Always call await ws.accept() before sending or receiving
WebSocket endpoints loop forever — they keep the connection alive
Wrap the loop in try/except WebSocketDisconnect to handle client disconnections cleanly
The Fix: Broadcasting with a ConnectionManager
Remember the original problem? Messages only went back to the sender. To build a real chat, you need to track all active connections and broadcast messages to everyone. That's what a ConnectionManager does.
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
class ConnectionManager:
def __init__(self):
self.active_connections: list[WebSocket] = []
async def connect(self, ws: WebSocket):
await ws.accept()
self.active_connections.append(ws)
def disconnect(self, ws: WebSocket):
self.active_connections.remove(ws)
async def broadcast(self, message: str):
# Send to EVERY connected client
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@app.websocket("/ws/chat")
async def chat(ws: WebSocket):
await manager.connect(ws)
try:
while True:
data = await ws.receive_text()
# Now EVERYONE gets the message!
await manager.broadcast(f"User says: {data}")
except WebSocketDisconnect:
manager.disconnect(ws)
await manager.broadcast("A user has left the chat")broadcasting
What you just learned
A ConnectionManager tracks all active WebSocket connections in a list
broadcast() iterates through every connection and sends the message to each one
When a client disconnects, remove them from the list to avoid sending to dead connections
See It In Action
Three clients connect to the server. External events — webhooks, scheduled jobs, system alerts — arrive at the server, which broadcasts them to all connected clients in real-time. Watch what happens when a client disconnects mid-stream.
Sending Structured Data
Real apps don't just send text strings. Use receive_json() and send_json() for structured data.
@app.websocket("/ws/updates")
async def live_updates(ws: WebSocket):
await ws.accept()
try:
while True:
# Receive structured data from client
data = await ws.receive_json()
# data = {"action": "subscribe", "channel": "prices"}
# Send structured response back
await ws.send_json({
"channel": data["channel"],
"price": 42.50,
"timestamp": "2024-01-15T10:30:00Z",
})
except WebSocketDisconnect:
print("Client disconnected")Go Deeper: Heartbeats & Dead Connections
WebSocket connections can die silently. The client loses WiFi, the browser tab crashes, a proxy times out. Without heartbeats, your server holds onto dead connections forever, leaking memory.
Uvicorn handles protocol-level pings automatically. You just configure the interval:
# Uvicorn sends automatic keepalive pings:
uvicorn main:app \
--ws-ping-interval 20 \ # Ping every 20 seconds
--ws-ping-timeout 20 # Close if no pong within 20 seconds
# The client's browser handles Pong responses automatically
# — you don't need any client-side code for this.Ping / Pong in Action
Watch Uvicorn send periodic pings to keep the connection alive. When the client stops responding, the server detects the dead connection and cleans it up.
Ping / Pong Heartbeat
how Uvicorn detects dead connections
Application-Level Heartbeats
Sometimes protocol-level pings aren't enough. You might need to detect stale sessions, refresh auth tokens, or measure latency. Here's how to add your own heartbeat logic:
import asyncio
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
async def send_heartbeats():
"""Send application-level heartbeat every 30s"""
while True:
await asyncio.sleep(30)
try:
await ws.send_json({"type": "heartbeat", "ts": time.time()})
except Exception:
break # Connection is dead
# Run heartbeat alongside the message loop
heartbeat_task = asyncio.create_task(send_heartbeats())
try:
while True:
data = await ws.receive_json()
if data.get("type") == "pong":
continue # Client responded to our heartbeat
await handle_message(data)
except WebSocketDisconnect:
heartbeat_task.cancel()
print("Client disconnected")Cleaning Up Stale Connections
With a ConnectionManager, you should periodically close connections that haven't responded to heartbeats.
import time
class ConnectionManager:
def __init__(self):
self.connections: dict[WebSocket, float] = {} # ws → last_seen
async def connect(self, ws: WebSocket):
await ws.accept()
self.connections[ws] = time.time()
def disconnect(self, ws: WebSocket):
self.connections.pop(ws, None)
def mark_alive(self, ws: WebSocket):
self.connections[ws] = time.time()
async def cleanup_stale(self, max_age: float = 60):
"""Remove connections not seen in max_age seconds"""
now = time.time()
stale = [ws for ws, last in self.connections.items()
if now - last > max_age]
for ws in stale:
self.disconnect(ws)
try:
await ws.close(code=1001, reason="Heartbeat timeout")
except Exception:
pass # Already deadheartbeats & cleanup
What you just learned
Uvicorn handles protocol-level pings automatically with --ws-ping-interval
Application-level heartbeats let you detect stale sessions and measure latency
Track last_seen timestamps and periodically close connections that go silent
Think about it...
What happens to a WebSocket connection if the server restarts? Does the client automatically reconnect?
Hint: Think about what the WebSocket protocol defines vs what you have to build yourself.
Key Points
@app.websocket()
Declare WebSocket endpoints just like HTTP routes
Accept First
Always call await ws.accept() before sending or receiving
WebSocketDisconnect
Catch this exception to handle client disconnections cleanly
ConnectionManager
Track active connections for broadcasting to multiple clients
JSON Support
Use send_json() and receive_json() for structured data
Uvicorn --ws auto
Uvicorn supports WebSockets natively with configurable backends
Ping / Pong
Uvicorn sends automatic keepalive pings to detect dead connections
--ws-ping-interval
Control how often Uvicorn pings clients (default 20s, 0 to disable)
Stale Cleanup
Track last_seen timestamps and close connections that go silent