Socket.IO: rooms, namespaces and scaling

Updated 5 interview angles 5 min read source
On this page7
  1. The Python server
  2. Namespaces and rooms
  3. Authentication
  4. Scaling across workers
  5. When not to use it
  6. Related
  7. Interview angle

Socket.IO: rooms, namespaces and scaling

Socket.IO is not WebSocket. It is a protocol layered on top that adds reconnection, acknowledgements, rooms, namespaces and an HTTP long-polling fallback. Job descriptions name it specifically because those features are the ones teams end up needing.

The Python server

python-socketio runs as an ASGI app, so it mounts alongside FastAPI rather than replacing it.

python
import socketio
from fastapi import FastAPI

sio = socketio.AsyncServer(
    async_mode="asgi",
    cors_allowed_origins=[],
)
api = FastAPI()
app = socketio.ASGIApp(sio, other_asgi_app=api)

The HTTP routes stay on api; Socket.IO owns /socket.io/. One process, one port, two protocols.

Namespaces and rooms

These two get conflated in interviews, and the distinction is worth stating cleanly.

Namespace Room
Purpose Separate the protocol Group connections
Chosen by The client, on connect The server, at runtime
Analogy A URL path A mailing list

A namespace is a separate communication channel over the same physical connection - /chat and /admin have their own handlers, their own auth, and their own event names. The client asks for one.

A room is a server-side set of session ids. The client cannot join one itself; it can only ask, and the server decides. That asymmetry is the security property, and it is the reason room membership is where authorisation belongs.

Authorising at join time

python
@sio.event(namespace="/chat")
async def join(sid, data):
    user = await current_user(sid)
    doc = data["doc_id"]
    if not await can_read(user, doc):
        raise ConnectionRefusedError("forbidden")
    await sio.enter_room(
        sid, f"doc:{doc}", namespace="/chat"
    )

async def on_edit(doc_id: str, patch: dict):
    await sio.emit(
        "patch", patch,
        room=f"doc:{doc_id}",
        namespace="/chat",
    )

Note the room name is derived from a resource id, checked once at join time. Emitting to doc:{id} is then safe without re-checking each message.

The per-session room

Every connection is automatically in a room named after its own sid, so sio.emit(..., room=sid) is how you reply to one client. Adding a user:{id} room on connect gives you the same thing across a user’s several tabs and devices.

Authentication

There is no browser API to set headers on a WebSocket handshake, so the token travels in the connect payload rather than an Authorization header.

python
@sio.event
async def connect(sid, environ, auth):
    token = (auth or {}).get("token")
    try:
        claims = decode_jwt(token)
    except InvalidTokenError:
        raise ConnectionRefusedError("bad token")
    await sio.save_session(sid, {"uid": claims["sub"]})

Raising ConnectionRefusedError rejects the handshake, which is what you want

  • an unauthenticated socket should never exist.

Two things people get wrong:

  • A query-string token leaks into access logs and proxy logs. Use the auth payload, which is sent in the connect packet body.
  • The token expires but the socket does not. A long-lived connection outlives a 15-minute JWT. Either re-verify periodically and disconnect, or push an expiry event and let the client reconnect with a fresh token. See WebSocket and SSE Authentication.

Scaling across workers

This is the part that breaks in production, and the reason is structural: a room is in-memory state in one process. Two Uvicorn workers means two disjoint sets of rooms, and a client on worker A never receives an emit issued on worker B.

The fix is a client manager backed by Redis, which turns every emit into a pub/sub message all workers consume.

python
mgr = socketio.AsyncRedisManager(
    "redis://cache:6379/0"
)
sio = socketio.AsyncServer(
    async_mode="asgi", client_manager=mgr
)

With that in place any worker - or a Celery task, or a separate publisher process - can emit into a room it holds no connections for.

Sticky sessions

Redis fixes fan-out. It does not remove the need for sticky sessions, because Socket.IO’s handshake is multi-request when it falls back to polling: the client polls, gets a session id, then upgrades. If those requests land on different workers, the second one does not recognise the session and the handshake fails in a loop.

Two ways out:

  • Sticky routing by cookie or IP hash at the load balancer, so a session stays on one worker.
  • Force the WebSocket transport (transports: ["websocket"] on the client), skipping polling entirely. One connection, one worker, nothing to pin - at the cost of the fallback for networks that block WebSocket.

The general scaling material - connection budgets, graceful shutdown, L4 versus L7 - is in Scaling WebSocket Applications.

When not to use it

Socket.IO costs you a non-standard protocol: only a Socket.IO client can talk to it, and the framing overhead is real. If you control both ends, need no fallback, and are building simple server push, plain WebSocket or even SSE is lighter. See WebSocket vs Alternatives.

Choose it when you want rooms, acknowledgements and reconnection without writing them, which is a reasonable trade for a collaborative app.

Interview angle 5

  • “Namespace or room?” - a namespace is a separate channel the client asks for on connect, with its own handlers and auth, like a URL path. A room is a server-side group of session ids that the client cannot join on its own. That asymmetry is why authorisation lives at room-join time.
  • “How do you authenticate a Socket.IO connection?” - send the token in the connect auth payload rather than a query string, which would leak into access logs, and reject with ConnectionRefusedError so an unauthenticated socket never exists. Then handle the token outliving the connection, by re-verifying or forcing a reconnect.
  • “You added a second worker and messages stopped arriving. Why?” - rooms are per-process memory, so worker B has no idea about a room on worker A. Attach a Redis client manager so emits fan out over pub/sub and any worker can publish into any room.
  • “Does Redis remove the need for sticky sessions?” - no. Redis fixes message fan-out; sticky sessions fix the handshake, which is multi-request when polling is used and fails if the requests hit different workers. Either pin sessions at the load balancer or force the WebSocket transport and drop the fallback.
  • “Why not plain WebSocket?” - Socket.IO is a non-standard protocol requiring its client and adding framing overhead. It earns that when you want rooms, acknowledgements, reconnection and a polling fallback for free; for simple server push, plain WebSocket or SSE is lighter.