python 97 lines · 3 tabs

FastAPI WebSocket Connection Manager for Broadcasting to Room Subscribers

Shared by codesnips Aug 2026
3 tabs
import asyncio
from collections import defaultdict

from fastapi import WebSocket


class ConnectionManager:
    def __init__(self):
        self._rooms: dict[str, set[WebSocket]] = defaultdict(set)
        self._lock = asyncio.Lock()

    async def connect(self, room: str, websocket: WebSocket) -> None:
        async with self._lock:
            self._rooms[room].add(websocket)

    async def disconnect(self, room: str, websocket: WebSocket) -> None:
        async with self._lock:
            members = self._rooms.get(room)
            if members:
                members.discard(websocket)
                if not members:
                    self._rooms.pop(room, None)

    async def broadcast(self, room: str, payload: dict) -> None:
        async with self._lock:
            targets = list(self._rooms.get(room, ()))

        if not targets:
            return

        results = await asyncio.gather(
            *(ws.send_json(payload) for ws in targets),
            return_exceptions=True,
        )

        stale = [ws for ws, res in zip(targets, results) if isinstance(res, Exception)]
        for ws in stale:
            await self.disconnect(room, ws)


manager = ConnectionManager()
3 files · python Explain with highlit

A WebSocket connection manager centralizes the bookkeeping that a real-time feature needs: tracking which sockets are open, grouping them by room, and fanning a single message out to every subscriber. This snippet shows the pattern with three collaborating pieces — the manager that owns the socket registry, a Pydantic message envelope, and the FastAPI endpoint that wires clients into a room.

In ConnectionManager, the core data structure is self._rooms, a mapping from a room name to a set of live WebSocket objects. A set is used rather than a list so that add and remove are O(1) and duplicate registration is harmless. Every mutation is guarded by an asyncio.Lock, because connect and disconnect can interleave with a broadcast running on another task; without the lock a socket could be closed and removed while the broadcast loop is still iterating over it, raising a RuntimeError: Set changed size during iteration.

The broadcast method snapshots the room's members under the lock, then sends outside the lock. This is deliberate: holding the lock across await ws.send_json(...) would serialize all sends and let one slow client block everyone else. Instead the sends are dispatched concurrently with asyncio.gather(..., return_exceptions=True), so a single dead connection surfaces as an exception in the results rather than aborting the whole fan-out. Dead sockets collected in stale are then reaped in a follow-up disconnect pass — a simple form of failure handling that keeps the registry from leaking closed connections.

ChatMessage in schemas defines the wire envelope with model_validate used to parse untrusted client input; malformed payloads raise ValidationError which the endpoint turns into a clean error frame instead of tearing down the socket.

chat routes shows the idiomatic FastAPI flow: await websocket.accept(), register with the manager, then loop on receive_json(). The WebSocketDisconnect exception is the normal termination path and simply triggers cleanup in the finally block. A pitfall worth noting is that this manager holds state in a single process, so it only broadcasts to clients on the same worker — scaling horizontally requires a shared backplane such as Redis pub/sub feeding each instance's broadcast.


Related snips

Share this code

Here's the card — post it anywhere.

FastAPI WebSocket Connection Manager for Broadcasting to Room Subscribers — share card
Link copied