DDSA Solutions
Case Study6 min read·

Design a Real-Time Leaderboard

How to design a gaming or app leaderboard for interviews: Redis sorted sets, score updates, top-K queries, sharding by game, and handling ties at scale.

"Design a leaderboard" shows up in gaming companies, fitness apps, and anywhere users compete on scores. The core operations sound simple — update a score, show top 10, show my rank — but doing that millions of times per second with correct ordering is a classic Redis interview question. It also connects to sorted-set intuition from LeetCode (heaps, priority queues) without writing a single `PriorityQueue` in C#.

Use the interview framework to pin down scope: global leaderboard vs friends-only, whether scores only go up or can decrease, and how fresh ranks must be (real-time vs eventual).

Requirements

  • Update player score when they finish a match or level.
  • Fetch top N players (e.g. top 100) for a leaderboard scope.
  • Fetch a specific player's rank and score.
  • Support multiple leaderboards: daily, weekly, all-time, per-game mode.
  • Handle ties consistently (same score → tie rank or secondary sort by timestamp).

Clarify tie-breaking

If two players have score 9000, who ranks higher? Common rule: earlier achiever wins (store composite score `score * 1e13 + (MAX_TS - achieved_at)` in a single sorted-set member value), or show shared rank #5 with next player at #7.

Why not SQL ORDER BY?

`SELECT * FROM scores ORDER BY score DESC LIMIT 10` works for prototypes. At 10M players and 50K score updates per second, maintaining a global index on score becomes a write bottleneck and `LIMIT 10` still scans a large B-tree. Reads of top-K are fast; writes that reshuffle the index are not. In interviews, say SQL for durability and Redis for hot ranking — polyglot persistence from our SQL vs NoSQL guide.

Redis sorted sets (ZSET)

Redis ZSET maps member → score with O(log N) insert and O(log N + M) for top-M. Commands: `ZADD leaderboard:weekly user123 4500`, `ZREVRANGE leaderboard:weekly 0 9 WITHSCORES`, `ZREVRANK leaderboard:weekly user123`. One key per leaderboard scope. This is the answer interviewers want for real-time global boards under ~100M players per board.

Architecture

  1. Game client sends score event to API after match ends.
  2. API validates, writes durable record to Kafka (optional audit).
  3. Worker updates Redis ZSET and sets TTL on daily/weekly keys.
  4. Read API serves top-N and rank from Redis; cache miss rebuilds from DB (slow path).
  5. Periodic snapshot job persists Redis top 10K to PostgreSQL for history.

Composite score for tie-break

Store `display_score` in API responses but sort by `sort_score = score * 1e10 - timestamp_ms` so higher game score wins and earlier time wins ties. Document this in the API so clients do not reverse-engineer ranks incorrectly.

Sharding large leaderboards

A single Redis node holds ~100M members comfortably for ZSET ops, but memory caps out. Shard by `hash(user_id) % num_shards` only if you partition users — global top-10 then needs merge across shards (expensive). Better: shard by `game_id` or `region` so each board is self-contained. For a true planet-scale single board, use Redis Cluster with one key per board and vertical scale first; mention consistent hashing when adding nodes.

Friends leaderboard

Do not maintain a separate ZSET per user's friend graph (combinatorial explosion). On read: fetch friend IDs from social graph service, `ZMScore` or pipelined `ZSCORE` for each friend, sort in app memory if friend count < 500. For larger graphs, maintain a friends-only ZSET updated on each friend's score change via async fan-out — trade write amplification for faster reads.

Time-windowed boards (daily / weekly)

  • Key naming: `lb:daily:2026-07-08`, `lb:weekly:2026-W27`.
  • Set TTL 8 days on daily keys so Redis reclaims memory automatically.
  • Rolling window alternative: store score events in a stream and aggregate — heavier, use only if rules are complex.

API examples

EndpointBehavior
POST /v1/scoresBody `{ user_id, game_id, score }` → update board
GET /v1/leaderboards/{id}/top?n=100Return ranked list with ties handled
GET /v1/leaderboards/{id}/users/{user_id}Return rank, score, percentile

Protect write endpoints with rate limiting — score cheating bots are common.

Failure modes

  • Redis down: serve stale rank from last snapshot; queue score updates in Kafka.
  • Split brain on Redis Cluster: prefer short unavailability over double-counted ranks.
  • Hot key on one global board: read replicas for ZSET reads if using Redis Enterprise or custom read fan-out.

Worked example: weekly board

  1. Player `u42` scores 12,400 in a match.
  2. API publishes `ScoreEvent` to Kafka; consumer runs `ZADD lb:weekly:2026-W27 u42 <sort_score>`.
  3. Client opens leaderboard: `ZREVRANGE` top 100 → hydrate usernames from user service batch API.
  4. Player checks rank: `ZREVRANK` returns 847 → show "Top 8%" if you precompute percentiles offline.
  5. Sunday midnight UTC: new key `lb:weekly:2026-W28`; old key expires in 8 days.

Percentile ranks without scanning everyone

Exact rank from ZREVRANK is O(log N). "Top 5%" for 10M players without O(N) scan: maintain coarse histogram buckets (score 0–999, 1000–1999, …) updated on each ZADD, or approximate with Redis `ZCOUNT` between score ranges. For interviews, ZREVRANK plus exact rank is enough; mention approximation only if interviewer asks about percentiles at scale.

Cheating and integrity

  • Server-side score validation — never trust client-reported points blindly.
  • Detect impossible score jumps (0 to 999999 in one second).
  • Rate limit score submissions per user per minute.
  • Ban list synced to API layer before ZADD.

Memory math

Rough ZSET cost: 10M members × ~64 bytes overhead ≈ 640 MB plus member strings. Fits one Redis node with headroom. 100M members needs cluster or archival of inactive players to cold storage. Archive users inactive 90 days: remove from hot ZSET, restore on next login if they play again.

ScaleApproach
< 1M players per boardSingle Redis ZSET
1M–50MRedis Cluster, one key per board
50M+ globalRegional boards + optional merge job
Friends onlyRead-time ZMScore on friend list

Do not over-build

Skip custom skip-list microservice unless interviewer forces it. Redis ZSET is the industry answer for gaming leaderboards at most companies you will interview with.

Global and regional leaderboards

Separate ZSET per region (`lb:weekly:EU`, `lb:weekly:APAC`) keeps boards fair and shards naturally. A "global" board merging regions either runs periodic merge job (union top 1000 from each) or maintains a third ZSET fed by all score events — doubles write path. For interviews, regional boards plus optional global snapshot is a clean answer.

Persistence: Redis is in-memory — snapshot ZSET to disk hourly or replay Kafka score events to rebuild after Redis failure. Leaderboard data is not as critical as payments; brief empty board during rebuild may be acceptable with "scores updating" banner.

Interview closing

Lead with ZSET operations and complexity. Mention durable event log, TTL for time windows, tie-breaking trick, and friends board as a read-time aggregation problem. Connect to distributed cache eviction and memory limits. That shows you know the right tool without proposing a custom skip list service nobody wants to operate.

More in this series