technical2 min read·Jun 27, 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.

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.