Requirements
- Update a player score in real time
- Query the global top-100 leaderboard
- Query a player’s exact rank among all 100M players
- Query players ranked just above and below a given player
- 10K score updates per second
Core Data Structure: Redis Sorted Set
Redis sorted sets are purpose-built for leaderboards. All operations run in O(log N).
# Update player score (creates or updates atomically)
ZADD leaderboard:game1 {score} {player_id}
# Top 100 (highest scores first)
ZREVRANGE leaderboard:game1 0 99 WITHSCORES
# Player's 0-indexed rank (lower = better)
ZREVRANK leaderboard:game1 {player_id}
# Players around rank 50 (+/- 5)
ZREVRANGE leaderboard:game1 45 55 WITHSCORES
# Player's score
ZSCORE leaderboard:game1 {player_id}
Memory: ~100 bytes per member. 100M players = ~10GB RAM. Use Redis Cluster for horizontal scaling or a high-memory instance.
Data Model
Player(player_id, username, avatar_url, created_at)
Score(player_id, game_id, score, updated_at) -- persistent DB
Redis ZSET: leaderboard:{game_id} -- score -> player_id
Redis HASH: player:{player_id} -- username, avatar (for fast display)
Score Update Flow
- POST /scores {player_id, game_id, score}
- Validate: score not negative, not lower than existing (no score regression)
- Persist to DB: INSERT … ON CONFLICT DO UPDATE SET score = GREATEST(score, excluded.score)
- Update Redis: ZADD leaderboard:{game_id} {score} {player_id}
- Publish to Kafka for analytics and achievement triggers
GREATEST prevents score regression from out-of-order updates. ZADD updates atomically — no read-modify-write needed.
Time-Windowed Leaderboards
Separate sorted sets per time window: ZADD leaderboard:{game_id}:daily:{YYYYMMDD}. Set EXPIREAT for automatic cleanup after the display window. On each score update, write to both the all-time and current daily/weekly sorted sets. To query yesterday’s top-100: ZREVRANGE leaderboard:{game_id}:daily:{yesterday}.
Displaying the Leaderboard
Top-100 display: ZREVRANGE returns 100 player_ids. Pipeline 100 HGET player:{id} for username/avatar in one round trip. Cache the rendered JSON: key=lb_top100:{game_id}, TTL=5 seconds. Invalidate when any top-100 member’s score changes. For 100M players, the top-100 barely changes second-to-second, so 5s staleness is fine.
Friend Leaderboard
Fetch scores for all friend IDs using ZMSCORE leaderboard:{game_id} friend_id_1 friend_id_2 … (up to ~5000 friends). Sort the result client-side or in the application layer. Faster than maintaining a separate friend sorted set per user. For players with many friends (social games), compute offline and cache per user.
Key Design Decisions
- Redis ZSET is the canonical leaderboard structure — never use SQL ORDER BY on 100M rows
- GREATEST in DB prevents stale out-of-order updates from lowering a player’s score
- Separate ZSET keys per time window with EXPIREAT for daily/weekly boards
- Cache rendered top-N with short TTL to reduce Redis round trips under high read traffic
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”What data structure should you use for a real-time leaderboard?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Redis sorted set (ZSET). It maintains elements sorted by a floating-point score with O(log N) insertion, deletion, and rank queries. Key operations: ZADD leaderboard score player_id (insert or update), ZREVRANK leaderboard player_id (0-indexed rank from highest), ZREVRANGE leaderboard 0 99 (top 100), ZSCORE leaderboard player_id (get current score). A sorted set with 100M members uses ~10GB RAM — manageable on a high-memory instance or Redis Cluster. Never use a SQL table with ORDER BY for a real-time leaderboard at this scale — full table scans or index traversal can't match Redis's O(log N) for both writes and rank queries.”}},{“@type”:”Question”,”name”:”How do you implement time-windowed leaderboards (daily, weekly)?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Maintain separate sorted sets per time window: ZADD leaderboard:{game}:daily:{YYYYMMDD} score player_id. On each score update, write to both the all-time sorted set and the current daily/weekly sorted set. Set EXPIREAT on time-windowed keys to auto-delete after the display window closes (e.g., daily key expires after 8 days so last week is still queryable). To query yesterday's top 100: ZREVRANGE leaderboard:{game}:daily:{yesterday} 0 99. Weekly: same pattern with a weekly key. For season-based leaderboards, create a new key per season and keep historical season keys indefinitely (or archive to cold storage).”}},{“@type”:”Question”,”name”:”How do you prevent score regression from out-of-order updates?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Two mechanisms: (1) In the database: INSERT INTO scores (player_id, game_id, score) VALUES (%s, %s, %s) ON CONFLICT (player_id, game_id) DO UPDATE SET score = GREATEST(scores.score, excluded.score). GREATEST ensures the DB score only increases. (2) In Redis: before ZADD, check current score with ZSCORE. Only update if new_score > current. Or use Lua script for atomic check-and-update: if new score > current, call ZADD. However, brief inconsistency between DB and Redis is acceptable — the authoritative score is in the DB; Redis is the display cache. On server restart, rebuild the Redis sorted set from DB.”}},{“@type”:”Question”,”name”:”How do you show a player their rank among friends?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”For small friend lists (under 5000): use ZMSCORE leaderboard:{game} friend_id_1 friend_id_2 … to fetch all friend scores in one Redis call. Sort the result in application code and find the player's position. For large friend lists: maintain a separate sorted set per user key=friend_board:{user_id}, updated when a friend's score changes. Fan-out on friend score change: for each player P, for each of P's friends F, update friend_board:{F}. Expensive if users have many friends. Compromise: precompute friend leaderboards hourly in a batch job instead of real-time, acceptable for casual games.”}},{“@type”:”Question”,”name”:”How do you handle a leaderboard for 100 million players in one Redis instance?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”A Redis sorted set with 100M members uses approximately 10GB RAM (each member: ~50 bytes score + ~50 bytes member string). This fits in a single high-memory instance (r6g.2xlarge has 64GB). For higher throughput: use Redis Cluster with multiple shards, each handling a partition of players. But cluster ZREVRANK across shards requires cross-shard coordination. Alternative: hash players into N sub-leaderboards (player_id mod N), accept approximate global ranks (rank in sub-leaderboard * N + offset). For true global ranking across shards: maintain a summary sorted set with shard-level scores, two-level lookup.”}}]}
Snap system design covers real-time leaderboards and ranking. See common questions for Snap interview: leaderboard and ranking system design.
Twitter system design covers trending and ranking systems. Review design patterns for Twitter/X interview: leaderboard and trending system design.
Amazon system design covers leaderboards and real-time ranking. See design patterns for Amazon interview: game and ranking system design.