TL;DR: One Redis sorted set per game or season answers scores, global rank, and nearby players with O(logN) rank lookups and O(logN+M) range reads returningMmembers; shard by game, never by player, because rank is a property of the whole set. Pipelined updates absorb score storms, and cohort filters mark where this design stops.
How to approach it
Name the three query shapes first: submit a score, read the top 100, read a player's own rank and neighbours. Then size the write path, because update volume, not read volume, is what stresses this design.
A strong answer
Mechanics. A Redis sorted set does all three natively: ZINCRBY updates, ZREVRANGE 0 99 WITHSCORES for the podium, ZREVRANK for personal rank. Updates and rank lookups are O(log N); range reads additionally cost the number of returned members. Big-O does not establish a microsecond latency bound. The reason this primitive matters is what it replaces: a relational ORDER BY score DESC LIMIT 100 re-sorts or scans an index on every read, and a personal rank becomes a COUNT(*) WHERE score > mine, which is linear in the number of players above you. The sorted set maintains the ordering as a side effect of the write instead.
Capacity arithmetic. Ten million daily players submitting twenty score events each is 200 million updates a day, about 2,300 per second on average, peaking near 45,000 during a tournament final. Benchmark the real sorted-set size, member bytes, persistence and pipeline depth; a simple-command benchmark does not prove 45k updates/s capacity. Size memory including allocator/index overhead and a replica. Reads mostly go to replicas since leaderboards tolerate slightly stale ranks. One primary plus one replica is the honest footprint, and saying so beats inventing a cluster.
Details that separate practitioners:
- Ties. Redis scores are double-precision numbers. If packing score and timestamp, prove the entire integer encoding stays within 2^53 and define range/overflow behavior; otherwise use an explicit bounded tie-break design or accept lexicographic member ordering for equal scores.
- Seasons. Namespace keys per season (
lb:s42), archive to the database at close, set TTLs. Un-namespaced boards grow into unmigratable monsters. - Durability. Scores are derivable only with replay-safe updates: deduplicate event IDs atomically with ZINCRBY, or apply authoritative versioned absolute scores. Replaying raw increments doubles points. AOF with every-second fsync is enough; losing a second of increments beats stalling writes.
Options compared:
| Approach | Global rank query | Update cost | Fits when |
|---|---|---|---|
| Redis sorted set | Exact, O(log N) | O(log N) | Real-time boards, seasons, neighbour views |
| SQL ORDER BY on indexed score | Exact, slows as N grows | Cheap | Low traffic, heavy filtering needs |
| Precomputed buckets or percentiles | Approximate | Batch | Tens of millions of players, fuzzy rank acceptable |
Where it stops working. Friends-only or city-filtered boards break the single-set model. Either maintain a sorted set per cohort and pay the fan-out write cost, or compute cohort ranks offline and accept staleness. Name that reversal yourself; the interviewer is waiting to ask.
What interviewers probe next
"A score storm doubles your peak. What breaks first?" The single-threaded primary saturating on command volume. Mitigate with pipelining, then read replicas; beyond one node, shard players into per-tier sets with periodic merges for approximate global ranks.
"Show me the players ranked just around me." ZREVRANK gives my position, then ZREVRANGE over a window around it fetches neighbours, with O(logN+M) work forMreturned neighbours.
"Redis restarts mid-tournament." Promotion timing and acknowledged-update loss depend on the failover and persistence configuration and the board rebuilds from the gameplay event stream; scores are derived data, so treat them that way.
Common mistakes
Sharding by player ID, then scattering every leaderboard view across all shards to re-rank, which puts scatter-gather on the hot path.
Keeping a rank column in SQL updated per play, rewriting thousands of rows on every score change.
Ignoring tie-breaking until players notice identical scores flipping order between refreshes.
References