Sports traffic is a brutal test case for system design. Nothing happens for 89 minutes, then a goal fires and every connected client wants an update in the same second. I hit this directly building a World Cup 2026 companion app — live scores, match analysis, notifications — using Next.js, Socket.io, and Redis-backed job queues (BullMQ).
This isn't a system handling millions of concurrent users. But the design questions that traffic pattern forces on you are the same ones you'd face at much larger scale — you just get more room to experiment before it actually breaks. Here's what I learned about async processing and real-time delivery.
1. Async-first beats synchronous-and-hope
Handling bursty traffic synchronously — API request comes in, app does the work, app responds — falls over exactly when it matters most, because your spikes are correlated with the thing your users actually care about. A missed goal notification during the one minute it mattered is worse than no feature at all.
So anything that wasn't required to happen in the request-response cycle got pushed into a queue: fetching and processing match events, fanning out notifications, running post-match analysis. The web server's job became "accept the request fast and hand off the work," not "do the work." Redis-backed queues gave me retry logic and backpressure almost for free — if a burst of events comes in faster than workers can process them, they queue instead of timing out or crashing the server.
The tradeoff is real: async systems are harder to reason about, harder to debug (where did this job go?), and you now have eventual consistency to think about instead of "it just happened." But for anything bursty, that tradeoff is almost always worth it over a synchronous system that's simpler right up until the one moment it isn't.
2. Real-time doesn't mean "broadcast to everyone"
Socket.io makes it dangerously easy to broadcast every update to every connected client. That works fine in a demo. It does not work once you have thousands of people watching different matches, because now every client's browser is filtering out most of the messages it receives just to find the one it cares about.
The fix was scoping connections into rooms — one per match — so a client only receives events for what it's actually watching. This is an obvious idea in hindsight, but it's the kind of thing that's easy to skip when you're focused on "does real-time work at all," and only becomes a problem once your Socket.io server is doing meaningfully more work than it needs to per connected client.
The general principle: real-time infrastructure scales by what you don't send, not just by how fast you send it. Scoping and targeting your broadcasts is usually a bigger scalability lever than optimizing the transport itself.
3. Your real bottleneck is probably someone else's API
The app leans on third-party services for live data, AI-powered analysis (Gemini), and billing (Stripe) — each with its own rate limits and failure modes that don't show up until traffic actually spikes. A burst of match events can mean a burst of downstream calls to generate analysis or trigger notifications, and none of those upstream services promise to absorb that burst gracefully just because your own queue can.
That pushed rate-limiting and backoff logic down into the worker layer itself, not just the request layer — workers pulling jobs off the queue needed to respect external rate limits independently of how fast jobs were arriving. The queue smooths out your bursts; it doesn't automatically smooth out the burst you're about to hand to someone else's API.
The lesson: when you sketch your architecture diagram, the boxes representing external APIs deserve as much design attention as the boxes you control. Queueing your own work is necessary but not sufficient — you have to think about what your workers do when they hit someone else's limits too.
4. Cheap infrastructure, disciplined architecture
This system runs on free/low-tier hosting, not a fleet of load-balanced servers — because that's the reality of building as a student. That constraint was useful. It forced decisions that scale regardless of the infrastructure underneath: stateless services that don't care which instance handles a request, background work that lives in a queue instead of memory, and real-time connections scoped tightly enough that they don't waste bandwidth they don't need.
The uncomfortable truth is that most "scalability" problems people reach for infrastructure to solve are actually architecture problems. A tightly-coupled, synchronous system doesn't get more scalable by throwing more servers at it — it just gets more expensive in the same shape. Fixing the shape first, on cheap infrastructure, means that if I do need to scale the infrastructure later, I'm scaling something already designed to be scaled.
Takeaways
- Push bursty, non-critical-path work into queues — synchronous systems fail exactly when traffic spikes matter most.
- Scope real-time broadcasts — sending less is usually a bigger lever than sending faster.
- Rate-limit at the worker layer, not just the request layer — your queue smooths your own bursts, not the ones you hand downstream.
- Fix the architecture before reaching for infrastructure — cheap hosting with disciplined design scales further than you'd expect.
None of this required expensive infrastructure or massive traffic to learn — it just required paying attention to why a design choice mattered, not just whether it worked in the demo. That's the part that transfers, regardless of what you're building next.
I'm a final-year CS student building AI-integrated MERN stack applications. Part one of this series covers the multi-tenant RAG side of things, built for a very different kind of load — drop a comment if you've hit similar queueing or real-time tradeoffs.
Top comments (0)