A FastAPI WebSocket endpoint must accept the handshake, authenticate the connection, handle disconnects, and bound every queue; the echo loop is only the first ten lines.
Long-lived connections change failure behavior. Deployments, proxies, slow clients, and multiple workers all become part of the protocol.
Table of contents
- Build the smallest correct endpoint
- Authenticate the handshake
- Handle backpressure and slow clients
- Survive proxies, restarts, and multiple workers
- Operate the connection lifecycle
- How this fits the rest of the stack
- FAQ
Build the smallest correct endpoint
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket('/ws')
async def socket(websocket: WebSocket):
await websocket.accept()
try:
while True:
message = await websocket.receive_text()
await websocket.send_json({'echo': message})
except WebSocketDisconnect:
pass
Accept before sending application messages, and catch disconnects so registry cleanup runs. Validate message shape and size before processing. A client controls when data arrives, so every receive loop needs cancellation and resource limits rather than assuming polite traffic.
Authenticate the handshake
Browsers cannot attach every custom header pattern used by ordinary API clients. Common options include a secure cookie, a short-lived query token, or a subprotocol negotiation. Validate origin where relevant and never place a long-lived credential in a URL that may enter logs and history.
Authorize the specific channel or resource after identity is known. A user permitted to connect is not automatically permitted to subscribe to every project. Close with a deliberate policy code when authentication fails instead of accepting and silently ignoring messages.
Handle backpressure and slow clients
One slow connection must not block an event producer or grow an unbounded list. Give each connection a bounded queue, define whether old messages are dropped or the client is disconnected, and record that decision in metrics. Limit message size and rate before expensive parsing.
Send operations can block when the network is congested. Use timeouts around application-level delivery and isolate broadcast work so one stalled browser does not delay every healthy client. Real-time does not mean infinite buffering; it means a clear latency and loss contract.
Survive proxies, restarts, and multiple workers
The reverse proxy must support HTTP upgrade, preserve required headers, and allow an idle timeout longer than the heartbeat interval. During deployment, clients will disconnect. Implement reconnect with exponential backoff and application-level resynchronization rather than promising an immortal socket.
An in-memory connection registry belongs to one process. With multiple workers or instances, distribute events through a broker or pub-sub service and keep presence semantics honest. Sticky sessions may reduce routing changes but do not replace shared event delivery.
Operate the connection lifecycle
- Track active connections and handshake failures
- Measure queue depth, send latency, drops, and disconnect codes
- Use ping or application heartbeats below proxy idle limits
- Cancel per-connection tasks on disconnect
- Drain connections during deployment
- Test reconnect and duplicate-event handling
WebSocket systems fail in the gaps between components. Load-test long-lived connections, simulate a worker restart, throttle a client, and verify that authorization is rechecked when credentials expire. A working chat demo is not yet an operating model.
Define the message protocol independently from the socket transport. Give every event a type, version, identifier, and documented payload, then decide how clients recover when they miss an event. A monotonically increasing cursor or last-known revision lets a reconnecting client request the gap through HTTP or a fresh snapshot. Without that recovery path, reconnect simply creates a live connection with stale state. Validate incoming JSON before it reaches business logic and reject unknown message types deliberately. Cap subscription counts per connection and connections per identity, and apply rate limits at both handshake and message level. During shutdown, stop accepting new sockets, notify connected clients when practical, allow a short drain window, then close remaining connections with a retryable code. Test the protocol with two workers and a broker, because a single-process test cannot reveal cross-instance ordering or fan-out failures. The socket carries events; the application still owns consistency.
How this fits the rest of the stack
Real-time connections need runtime, network capacity, event delivery, and observability. The RunxBuild hosting calculator helps model those resources, and the RunxBuild dashboard keeps the service route and logs visible.
Useful related references:
- Render 部署 FastAPI 项目: The Deployment Checklist That Actually Matters
- FastAPI Logging That Survives Production: A Working Developer’s Guide
- Django vs FastAPI: An Honest 2026 Comparison for Backend Teams
- Database connection limits on RunxBuild
FAQ
Do I need to call websocket.accept?
Yes. Accept the handshake before normal application messages, unless you intentionally reject the connection.
How should a browser authenticate a WebSocket?
Use a secure cookie, short-lived query token, or subprotocol pattern, then authorize the requested resource.
What happens with multiple FastAPI workers?
Each has its own in-memory connections. Use shared pub-sub or a broker for cross-worker events.
How do I prevent slow clients from consuming memory?
Use bounded queues, send timeouts, message limits, and an explicit drop or disconnect policy.
Will WebSockets stay connected during deploys?
No guarantee. Clients must reconnect with backoff and resynchronize application state.