Real-time Gaming Leaderboard
Problem statement
Design a leaderboard for a game with millions of players that updates scores in real time and answers 'top 10', 'my rank', and 'players around me' within milliseconds.
Requirements
Functional
Update a player's score (increment or set).
Top-N players; a player's rank; N players around a rank.
Monthly/weekly leaderboards (reset) and all-time.
Optional: leaderboards per region/friends.
Non-functional
Rank queries < 10 ms; score updates visible immediately.
25M DAU, 50M score updates/day (peaks ~5k/s).
Highly available; scores never lost (persist).
Scale to 100M+ players in one leaderboard.
Back-of-the-envelope estimates
- 50M updates/day ≈ 600/s, peak 5k/s — trivial for Redis.
- 25M players in a sorted set: ~25M × (8 B score + 8 B id + skip-list overhead ~70 B) ≈ 2–3 GB → one Redis primary fits; use replicas for reads.
- Relational alternative:
SELECT COUNT(*) WHERE score > ?per rank query is O(n) → not viable at 10 ms.
API
POST /v1/leaderboards/{lb}/scores { userId, delta } → { score, rank }
GET /v1/leaderboards/{lb}/top?n=10
GET /v1/leaderboards/{lb}/users/{id}/rank
GET /v1/leaderboards/{lb}/around/{id}?k=5
Data model
Redis ZSET lb:{leaderboard_id}:{period} member=user_id score=points (ZINCRBY, ZREVRANGE, ZREVRANK)
scores_db (leaderboard_id, period, user_id, score, updated_at) -- durable copy, source for rebuilds
users (user_id, name, avatar) -- hydration
High-level design
- Game/service posts score changes to the leaderboard service (validated server-side; never trust client-reported scores).
- Redis sorted set per leaderboard/period:
ZINCRBYfor updates,ZREVRANGE 0 9 WITHSCORESfor top-10,ZREVRANKfor my rank,ZREVRANGE rank-k rank+kfor neighbours. All O(log n). - Durability: each update also appended to a DB/Kafka; Redis persistence (AOF everysec) plus replicas; on Redis loss, rebuild the ZSET from the DB.
- Periods: key per period (
:2024-06monthly); a new period is just a new key; old keys archived to the DB and expired. - Sharding for > ~100M members: partition by score range (each shard holds a band) or by user hash with a merge for top-N (top-N from each shard, merged). Score-range sharding makes rank = offset within shard + counts of higher shards (kept as small counters).
- Read path through replicas; hydrate user profiles from a cache.
Architecture diagram
Deep dives
Why a skip list (ZSET): log-time insert and rank; exactly the operations needed. A B-tree with subtree counts is the disk-based equivalent.
Ties: ZSET orders equal scores lexicographically by member; to break ties by time, encode score * 2^20 + (MAX_TS − ts) into the numeric score.
Anti-cheat: score deltas validated against game events; cap per-match deltas; audit trail in the DB.
Friends leaderboard: intersect the friend list with scores on read (ZSCORE per friend, sort client-side) — friend lists are small.
Bottlenecks & mitigations
Single primary write throughput → far above need; if exceeded, shard as above.
Top-N hammered by every client → cache the top-100 for 1 s in the service layer.
Rebuild time after Redis loss (25M ZADDs) → pipeline in batches of 1,000; ~1–2 minutes; serve stale from replica meanwhile.
Failure modes
- Redis primary crash: replica promoted; possible loss of the last second of updates (AOF everysec) — recovered from Kafka/DB replay.
- DB writer lag: leaderboard remains correct in Redis; durability window widens — alert.
- Period rollover bug: writes to the old key → detect via key naming validation; migrate with
ZUNIONSTORE.
Follow-up questions interviewers ask
How would you compute a percentile rank cheaply for 1B users?
How would you build a leaderboard that supports score decay over time?
How would you handle regional leaderboards with global aggregation?