Leaderboard Design — Race Conditions & Sharding Trade-offs
Concurrent ZADD calls caused vanishing scores during tournaments.
- A production leaderboard needs sorted sets, sharding, and atomic score updates
- Redis Sorted Sets (ZADD, ZRANGE) are the canonical data structure — O(log n) per update
- Global leaderboards require consistent hashing or range-based sharding
- Real-time updates use in-memory stores; periodic snapshots persist to DB
- Performance: single Redis node handles ~100k updates/s; shard across 10 nodes for 1M+ QPS
- Production risk: hot keys (e.g., rank 1) cause contention; use local caching or shard by score band
Imagine a school spelling bee where the teacher writes every student's score on a giant whiteboard, always keeping the list sorted so the best spellers are at the top. Now imagine 50 million students doing that simultaneously — you can't have one teacher with one whiteboard anymore. A leaderboard system is the engineering answer to that problem: how do you always know who's winning, instantly, at any scale, without the whiteboard catching fire.
Leaderboards are deceptively simple on the surface — just a sorted list of scores. But they're one of the most revealing system design questions an interviewer can ask, because a production-grade leaderboard for a game like Fortnite or a platform like Duolingo touches almost every hard problem in distributed systems at once: low-latency reads, high-throughput writes, consistency vs. availability trade-offs, and hot-key contention. The question isn't exotic — it's a lens that exposes how deeply you actually think.
The core problem is that sorting is expensive. Maintaining a globally consistent ranked list across millions of simultaneous score updates, while serving millions of 'what rank am I?' queries in under 10 milliseconds, is not something a simple SQL ORDER BY can handle. Naive approaches collapse under load in ways that are silent and insidious — rankings drift, scores get lost, and the leaderboard becomes a lie nobody notices until a player's 10,000-point climb disappears.
By the end of this article, you'll be able to walk into a system design interview and whiteboard a production-quality leaderboard: choosing the right data structure, explaining exactly why Redis sorted sets exist for this problem, handling the dense-rank vs. competition-rank distinction, sharding for global scale, dealing with score update atomicity, and knowing when to reach for approximate solutions. You'll also know the failure modes that catch senior engineers off guard.
What is Design a Leaderboard System?
Designing a leaderboard system means building a service that ingests score updates, maintains a globally sorted ranking, and serves queries for top N and individual rank — all under low-latency and high-throughput constraints. It's a common system design interview question because it forces you to choose the right data structure (Redis Sorted Sets), plan for sharding, handle atomicity, and decide on caching and consistency.
The naive approach — a relational database with ORDER BY score LIMIT 100 — collapses at thousands of concurrent writes. Each insert triggers a full table sort, locking rows and pegging CPU. Production leaderboards must avoid that entirely by keeping the ranking in-memory and sorted at all times.
Core Data Structure: Redis Sorted Sets
A Redis Sorted Set stores unique members each with a floating-point score. Internally, it's implemented as a skip list and a hash table, giving O(log n) for add/update/remove operations and O(log n + m) for range queries (like getting the top 100).
- When a game round ends, the game server sends
ZINCRBY leaderboard:global <score_increment> <player_id>. This atomically increments the score. - To get the top N players:
ZREVRANGE leaderboard:global 0 N-1 WITHSCORES.ZREVRANGEreturns members sorted descending by score. - To get a player's rank:
ZREVRANK leaderboard:global <player_id>.
This single data structure handles the core ranking logic. But it doesn't solve everything: you need to handle millions of concurrent writes, persistence, and global aggregation.
- The weight is the score; the marble's label is the player ID.
- When you change a marble's weight, it automatically repositions.
- You can ask the bucket for the heaviest 100 marbles at any time.
- The bucket uses a skip list internally, so reordering after a weight change takes O(log n).
Sharding Strategies for Global Leaderboards
When a single Redis sorted set grows beyond a node's capacity or write throughput, you must shard. There are two common approaches:
- Fixed shard count with consistent hashing – Partition the player ID space into N shards (e.g., 16). Each shard holds a subset of players. When reading the global top N, you query all shards for their top N, merge results, and return the final top N. This merge is fast if N is small (e.g., 100). You trade off perfect accuracy at the boundaries (players with same score across shards) but it's acceptable.
- Range-based sharding by score – Shard 0 holds scores 0-999, shard 1 holds 1000-1999, etc. This is simpler for reads: you only need to query the top few shards to get the global top N. But scores change over time, so players may move across shards. Need a rebalancing mechanism or use a proxy that routes by score range.
A third, less common approach: split by competition (e.g., each tournament ID is its own sorted set). This works when leaderboards are per-event, not global.
Handling Score Update Conflicts and Atomicity
When multiple game servers can update the same player's score concurrently, you risk lost updates. The classic read-modify-write pattern (read current score, compute new score, write back) is not atomic. Two servers may read the same value, add their increments, and last write wins — discarding one server's increment.
- Atomic increment: Redis ZINCRBY atomically adds to the existing score. No read needed.
- Lua scripting: For more complex updates (e.g., add score, then conditionally update metadata), use a Redis Lua script that runs atomically.
- Optimistic locking: Use WATCH to monitor a key and MULTI/EXEC to conditionally update; but this adds overhead and may fail under contention.
For systems that need external atomicity (e.g., across multiple databases), implement a distributed lock (via Redlock or etcd) around the player's score update.
Real-Time Updates and Caching
Users expect to see their new rank immediately after finishing a round. But updating the global leaderboard in real-time every second for millions of players is expensive. You need a tiered approach:
- Write path: Score update goes to Redis (in-memory, sub-millisecond). No DB write on every update.
- Read path: Top N leaderboard cached in a separate Redis string, refreshed every 5–10 seconds or invalidated on threshold changes (e.g., a new score enters the top 100).
- Persistence: Periodically (e.g., every minute) snapshot the sorted set to a relational DB for recovery. Use Redis AOF + RDB for safety.
- Scalable reads: If you have 100k+ reads per second on the top 100, add a local cache on the API server (e.g., in-memory cache with TTL of 2 seconds). Stale reads are acceptable for most games.
For real-time updates like 'you just dropped to rank 5,000', push notifications via WebSocket or polling every 10 seconds.
- The top 100 is read 100x more than any individual rank — cache it.
- Individual player rank queries can hit Redis directly with low latency.
- Update the cached top 100 only when a new top score enters the threshold.
- Stale cache (a few seconds) is acceptable; stale scores (minutes) cause complaints.
Consistency vs. Availability Trade-offs
In a distributed leaderboard, you must choose how consistent the rankings are. CAP theorem says you can't have both strong consistency and high availability under partitions.
- Eventual consistency: Score updates propagate asynchronously to all shards/replicas. Two users in different data centers may see different ranks for the same player for a few seconds. This is acceptable for most game leaderboards — the 'exact rank at that instant' isn't critical.
- Strong consistency: Use Redis cluster with transactions or a single master Redis. Writes must go to the master, reads from the master (no read replicas). This increases latency and reduces availability if the master fails.
- Causal consistency: Ensure that a player sees their own update immediately, but others may see it later. Implement by reading the player's own score from a local consistent store.
For competition leaderboards with money prizes, you need stronger guarantees. Use a single leaderboard per competition (no sharding), enforce atomic writes, and provide audit logs.
The Vanishing High Score
- Never assume read-then-write is atomic — use Lua scripting or transactions.
- Always protect critical counters against lost updates in distributed systems.
- Test with concurrent writers to catch race conditions before they reach production.
WRONGTYPE errors — the key may be a different data type. Run TYPE leaderboard:global to verify. Also check if Lua script timed out (SCRIPT KILL can recover).Key takeaways
Common mistakes to avoid
5 patternsUsing a relational database with ORDER BY LIMIT
No atomicity on score updates
Storing whole leaderboard in a single Redis key
Forgetting tie-breaking rules
Caching top N without invalidation
Interview Questions on This Topic
How would you design a leaderboard for a mobile game with 10 million daily active users? Walk me through your data structure choices and trade-offs.
Frequently Asked Questions
That's System Design Interview. Mark it forged?
5 min read · try the examples if you haven't