DEV Community

turboline-ai
turboline-ai

Posted on

The Part of FastAPI WebSockets Nobody Talks About

The Part of FastAPI WebSockets Nobody Talks About

Most WebSocket tutorials end right where the interesting problems begin.

You follow along, spin up a FastAPI server, connect a browser tab, send a message, watch it echo back. It works. Then you try to build something real and the tutorial has quietly abandoned you.

Here is what that gap actually looks like, and how to close it.

Getting the Basics Solid First

Before anything else, the foundation has to be right. A minimal FastAPI WebSocket endpoint looks like this:

from fastapi import FastAPI, WebSocket
from typing import List

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"Message: {data}")
    except Exception:
        manager.disconnect(websocket)
Enter fullscreen mode Exit fullscreen mode

This is clean, readable, and gets you to a working single-room chat in under 30 lines. The ConnectionManager pattern is a good instinct because it keeps state centralized and gives you a natural place to add behavior later.

Multi-room support follows logically from here. You replace the flat list with a dictionary keyed by room name, and the broadcast method takes a room argument. The structure barely changes, which is a sign the abstraction is holding up.

That is about where most tutorials stop. It is also about where production requirements start.

The Three Gaps That Actually Bite You

Authentication is not optional, but most examples treat it like it is.

HTTP endpoints get JWT validation almost automatically through FastAPI's dependency injection. WebSocket endpoints do not. The handshake is an HTTP upgrade request, which means you have one shot to validate credentials before the connection is accepted. Passing a token as a query parameter is common but puts credentials in server logs. A better pattern is validating a short-lived token on the initial HTTP handshake using a custom dependency, then rejecting the WebSocket upgrade before accept() is ever called if the token fails.

Disconnect handling is messier than a single except block.

Clients disappear in ways that are not always clean. Network drops, browser tab closes, mobile apps backgrounding all produce different disconnect signals. If your ConnectionManager only removes a socket on a caught exception, you will accumulate stale connections in your list and broadcast to sockets that no longer exist. At minimum, you want explicit handling for WebSocketDisconnect from Starlette, and you want your broadcast method to catch send failures individually rather than letting one bad connection kill the whole loop.

A single server process is a ceiling, not a floor.

The in-memory ConnectionManager works until you deploy more than one instance of your app. User A connects to pod one. User B connects to pod two. They are in the same room but broadcasting to different lists. They cannot see each other's messages.

Redis Pub/Sub is the standard answer here. Each server instance subscribes to a Redis channel for each active room. When a message arrives on any instance, it publishes to Redis, and all instances receive it and forward it to their local connections. The WebSocket handling stays mostly the same. You are just replacing the in-memory broadcast with a Redis publish and wiring up a background subscriber per instance on startup.

On the HTML/JS Client

Using a plain HTML file with a WebSocket constructor in a script tag is the right call when you are still figuring out the server side. It removes every variable except the one you are actually debugging.

The moment the backend logic feels stable, though, it is worth replacing that file with a proper frontend setup. A React or Vue component with a dedicated WebSocket hook gives you reconnection logic, connection state management, and a real event model instead of onmessage callbacks stapled to a global variable. The backend does not care about the switch. The client becomes dramatically easier to reason about.

The Actual Takeaway

FastAPI and WebSockets are a genuinely good combination. The framework gets out of your way, async support is first-class, and the developer experience from prototype to something that almost works is fast.

The jump from "almost works" to "works reliably under real conditions" requires caring about the pieces that do not show up in the happy path: authentication at the handshake, disconnects that do not corrupt your connection pool, and a pub/sub layer that survives horizontal scaling. None of it is complicated, but none of it writes itself. The tutorials that skip those parts are not wrong, they are just stopping early.

Top comments (0)