During the last football World Cup, we built a lightweight live-score mini app focused on fast match updates, prediction games, and group discussions.
The original goal was simple:
ultra-fast score updates
low mobile bandwidth usage
stable performance during traffic spikes
minimal server costs
What surprised us was how difficult the “goal moment” problem became.
Whenever a goal happened in a knockout-stage match, traffic exploded almost instantly.
A single scoring event could trigger:
massive websocket fan-outs
notification bursts
cache invalidations
ranking recalculations
prediction settlements
social sharing spikes
The UI itself was the easy part.
The real challenge was keeping latency low while avoiding infrastructure meltdowns.
Overall Architecture
Here’s the simplified architecture we ended up using:
+------------------+
| Match Provider |
+--------+---------+
|
v
+------------------------+
| Match Event Queue |
| Kafka / Redis |
+-----------+------------+
|
+-----------------+-----------------+
| |
v v
+----------------+ +----------------+
| Score Workers | | Ranking Worker |
+--------+-------+ +--------+-------+
| |
v v
+---------------+ +----------------+
| Redis Cluster | | Prediction DB |
+-------+-------+ +----------------+
|
v
+-------------------+
| WebSocket Gateway |
+---------+---------+
|
+---------+---------+
| |
v v
+----------+ +-------------+
| Mini App | | Mobile Web |
+----------+ +-------------+
The core idea was:
Redis handled short-lived hot data
MySQL stored historical match data
WebSocket handled live pushes
Kafka buffered bursts during peak events
The “Goal Spike” Problem
The biggest traffic spikes happened immediately after goals.
Not during kickoff.
Not during halftime.
Only goals.
Traffic patterns looked roughly like this:
Minute 0-20 → stable
Goal event → 12x websocket burst
VAR review → reconnect storms
Penalty shootout → total chaos
We originally used polling every 5 seconds.
That completely failed during high-profile matches.
The problems:
huge duplicate requests
unnecessary database reads
delayed score updates
battery drain on mobile
We replaced polling with websocket streams plus local incremental updates.
That reduced backend request volume by around 78%.
Why Mini Apps Were Difficult
A lot of developers outside China aren’t familiar with mini app ecosystems.
The constraints are surprisingly strict.
Some examples:
limited websocket connections
package size limits
aggressive memory recycling
cold-start delays
background execution restrictions
On older Android devices, reconnect storms became a serious issue.
During penalty shootouts, some devices opened multiple duplicate websocket sessions after network jitter.
That created ghost connections which kept consuming memory on the gateway layer.
We eventually added:
heartbeat validation
duplicate-session eviction
exponential reconnect backoff
local event deduplication
That stabilized things considerably.
Redis Became the Most Important Layer
Originally we treated Redis as a secondary cache.
That was a mistake.
During peak matches, Redis effectively became the real-time backbone of the entire system.
We used it for:
live scores
match timelines
ranking snapshots
prediction counters
websocket routing
hot room metadata
One optimization that helped a lot:
Instead of invalidating large leaderboard structures after every event, we switched to partial incremental ranking updates.
That reduced ranking recomputation time from several seconds to under 200ms.
CDN Optimization Helped More Than Expected
One unexpected bottleneck was actually images.
Especially:
national flags
player avatars
emoji reactions
sticker packs
During semifinals, image bandwidth briefly exceeded websocket traffic.
We fixed this by:
aggressive WebP compression
sprite merging
edge CDN caching
lazy-loading reaction assets
Cold-start speed improved noticeably afterward.
Prediction Systems Were Surprisingly Expensive
The prediction feature looked harmless at first.
Users could guess:
final score
first goal scorer
possession rate
match winner
The problem was settlement traffic.
When matches ended, millions of prediction calculations triggered almost simultaneously.
Our first implementation recalculated rankings synchronously.
That caused database lock contention immediately.
We eventually moved settlement into async queues and chunked ranking updates into batches.
That eliminated most timeout issues.
WebSocket Fan-Out Was the Hardest Part
The most difficult engineering problem was websocket broadcasting.
A single goal event might need to reach:
live score rooms
prediction rooms
chat rooms
trending feeds
push notification workers
Naively broadcasting everything everywhere quickly became too expensive.
We switched to segmented channels:
match:arg-vs-fra
match:eng-vs-ned
prediction:global
chat:room:3821
This reduced unnecessary fan-outs significantly.
What Failed
Several things worked terribly.
Polling
Completely unsustainable during peak traffic.Full leaderboard recalculation
Too expensive.Large websocket payloads
Mobile packet loss became noticeable.Synchronous push notifications
Created retry storms.Real-time chat moderation
Much harder than expected during controversial matches.
What Worked Well
A few things scaled surprisingly well:
Redis Pub/Sub
websocket segmentation
edge caching
event queues
incremental ranking updates
local optimistic rendering
One interesting UX optimization:
We updated the score instantly on the client before the full match timeline arrived.
Users perceived the app as much faster even though backend latency stayed roughly the same.
Traffic Numbers
Some approximate peak metrics during knockout-stage matches:
Metric Peak
Concurrent websocket connections ~180k
Match event fan-outs/minute ~12M
Redis ops/sec ~500k
CDN image requests/minute ~8M
Peak websocket bandwidth ~4.2 Gbps
The infrastructure costs were still lower than we originally expected because most traffic was transient and cache-heavy.
Biggest Lesson
The biggest lesson was:
Real-time sports traffic is extremely bursty.
Most systems look stable right up until a major goal happens.
Then everything breaks simultaneously.
Designing for average traffic is almost meaningless for live sports products.
You have to design for emotional moments.
That changes everything:
traffic patterns
reconnect behavior
sharing behavior
caching strategy
notification systems
moderation workload
If We Rebuilt It Today
We would probably:
use protobuf for websocket payloads
reduce payload sizes further
move more logic to edge workers
introduce regional websocket routing
precompute ranking snapshots
use CRDT-style synchronization for chat
We’d also invest more in observability earlier.
During the first week, debugging realtime issues without proper tracing was painful.
Final Thoughts
The most interesting part of building a football live-score system wasn’t sports data.
It was understanding how human behavior changes during high-emotion events.
Goals create synchronized traffic spikes unlike almost anything else on the internet.
And penalty shootouts are basically distributed systems chaos testing.
Top comments (0)