technical2 min read·Jun 25, 2026

Scaling WebSockets to 300+ Concurrent Users with FastAPI

How I resolved multi-worker process isolation conflicts and achieved sub-50ms sync in production.

FastAPIWebSocketsPythonProductionRedisArchitecture
ShareLinkedInX / Twitter

The Problem

When I deployed my attendance system, I hit a wall: Gunicorn's worker model meant each process had its own WebSocket connection pool. State wasn't shared between workers, causing session corruption for users who reconnected to a different worker.

The Architecture

The attendance system uses WebSockets for real-time sync between the teacher's dashboard (showing who's present) and student check-in screens. When 300+ students connect simultaneously during a lecture, every connection needs to receive updates within 50ms.

The Root Cause

Gunicorn spawns multiple worker processes for handling requests. Each process has its own memory space. When I stored WebSocket connections in a Python dict, each worker only knew about its own connections. If a student connected to Worker A, then their browser reconnected to Worker B, Worker B had no record of them.

My Solution

Step 1: Shared State with Redis

I moved the connection registry from in-process dicts to Redis. Each worker reads/writes to the same Redis instance, so connection state is shared.

python
import redis import json class ConnectionManager: def __init__(self): self.redis = redis.Redis(host='localhost', port=6379, db=0) self.active_connections: dict[str, WebSocket] = {} async def connect(self, user_id: str, websocket: WebSocket): await websocket.accept() self.active_connections[user_id] = websocket self.redis.sadd('connected_users', user_id) async def broadcast(self, message: dict): # Publish to Redis channel — all workers receive it self.redis.publish('attendance_updates', json.dumps(message))

Step 2: Pub/Sub for Cross-Worker Broadcasting

I used Redis Pub/Sub so when one worker receives an attendance update, all workers get notified and can push to their local connections.

Step 3: Connection Heartbeats

Added periodic pings to detect stale connections. If a WebSocket doesn't respond within 10 seconds, it's cleaned up from both local and Redis state.

Results

300+ concurrent connections sustained during peak lecture hours
Sub-50ms sync latency measured via client-side timestamps
Zero session corruption after the Redis migration
80% reduction in admin workload through automated bulk processing

Key Takeaway

The lesson wasn't about WebSockets — it was about understanding your deployment model. Gunicorn's pre-fork model is great for HTTP, but requires explicit shared state for stateful protocols like WebSockets. Always test with your production configuration, not just uvicorn --reload.

AG

Written by Ansh Gautam

Full-stack engineer building production systems with FastAPI, React, and AI/LLM integrations. Currently looking for backend engineering & AI integration roles.