A deep dive into how I architected a high-traffic WebSocket-driven platform for Chelsea FC supporters — and the hard lessons learned when things melted under live match load.
When live match traffic hit 100k concurrent users at kick-off, our initial architecture crumbled within minutes. Here's the complete post-mortem and how we rebuilt it.
The core problem was our naive approach to WebSocket connections. We were running a single Node.js instance with no load balancing, and every client maintained a persistent connection to the same server. The moment a goal was scored and 60k clients simultaneously requested match state updates, the event loop stalled.
Our solution: Redis Pub/Sub as a message broker between WebSocket servers, horizontal scaling with sticky sessions, and a Read Replica strategy for the database layer. We also introduced an event queue to throttle burst updates client-side.
The second major issue was our database query patterns. Every state update triggered a fresh SELECT across our Postgres instance — under load this created a thundering herd problem. We solved this by introducing a write-through cache with Redis and only querying Postgres on cache misses.
For the frontend, we moved from polling to a true event-driven model. Each client subscribes to a match-specific Redis channel through our WebSocket gateway. Updates are published once and fan out to all subscribers — O(1) work per update instead of O(n).
The result: 99.97% uptime across the last three Premier League match days, with p95 latency under 120ms even at peak concurrent load.