System Design Interview: How Would You Design a Real-Time Leaderboard for Millions of Users?

By Oleksandr Andrushchenko — Published on
0 Likes
0 Dislikes
System Design Interview: How Would You Design a Real-Time Leaderboard for Millions of Users?
System Design Interview: How Would You Design a Real-Time Leaderboard for Millions of Users?

A leaderboard looks simple at small scale: store each user's score, sort by score, and return the first 100 rows. At millions of users and thousands of score updates per second, that approach becomes expensive quickly.

A production leaderboard must support fast score updates, top-N queries, individual rank lookups, pagination around a player's position, deterministic tie-breaking, horizontal scaling, and recovery without turning the primary database into a sorting engine for every request.

Table of Contents

Define the Requirements

Before selecting Redis, Cassandra, PostgreSQL, or another technology, clarify the expected behavior.

Assume a gaming platform with:

  • 50 million registered players;
  • 10 million active players in a season;
  • 50,000 score updates per second during peak traffic;
  • millions of leaderboard reads per minute;
  • global and regional leaderboards;
  • top-100 queries;
  • individual rank lookup;
  • five players above and below a given player;
  • score updates visible within roughly one second.

The distinction between real-time and strongly consistent matters. A leaderboard can usually tolerate a player appearing one position behind for a few hundred milliseconds. A payment ledger generally cannot tolerate that kind of inconsistency.

This allows the architecture to prioritize throughput and latency over globally serialized writes.

Why a Leaderboard Is Hard at Scale

A relational database can easily store millions of user scores:

CREATE TABLE player_scores (
    player_id BIGINT PRIMARY KEY,
    score BIGINT NOT NULL,
    updated_at TIMESTAMP NOT NULL
);

The difficulty is repeatedly answering:

SELECT player_id, score
FROM player_scores
ORDER BY score DESC
LIMIT 100;

An index on score can make this query much faster, but the system also needs frequent score updates. Every update changes the ranking index, creating write amplification and contention on a structure that all users share.

Additional queries make the workload harder:

  • What is player 841's exact rank?
  • Who are the five players immediately above player 841?
  • What is the global top 100?
  • What is the top 100 for Texas?
  • What was the leaderboard for the previous season?

The central challenge is maintaining an ordered index that changes continuously while serving very frequent reads.

Choose the Data Model

A useful design separates authoritative game state from the data structure optimized specifically for ranking.

Authoritative Score Storage

The durable database stores the canonical player score and enough information to rebuild the leaderboard.

CREATE TABLE player_scores (
    player_id BIGINT NOT NULL,
    season_id BIGINT NOT NULL,
    score BIGINT NOT NULL,
    version BIGINT NOT NULL,
    updated_at TIMESTAMP NOT NULL,

    PRIMARY KEY (player_id, season_id)
);

This database provides durability, auditing, reconciliation, and recovery.

The leaderboard service should not necessarily execute expensive ranking queries directly against this database for every request.

Ranking Index

A separate ranking store maintains data in score order.

The ranking index is optimized for operations such as:

  • insert or update a score;
  • find a user's rank;
  • return a rank range;
  • return the highest-scoring users.

This separation resembles other system-design architectures where the write model and read model serve different access patterns.

Use a Sorted Data Structure for Ranking

The core data structure needs efficient updates and ordered range queries. Balanced trees, skip lists, sorted sets, and specialized ranking indexes are natural choices.

Redis Sorted Sets

Redis sorted sets are a common interview choice because each member has a numeric score and the structure remains ordered automatically.

A leaderboard key could represent one season:

leaderboard:season:2026

Each player becomes a member:

member = player_id
score  = leaderboard_score

The data structure supports both score updates and rank queries without sorting the entire dataset for every request.

Updating Scores

Suppose player 841 earns another 250 points.

An incremental update can conceptually perform:

ZINCRBY leaderboard:season:2026 250 player:841

The ranking structure repositions the player based on the new score.

If the authoritative service calculates the complete score instead, it can write the absolute value:

ZADD leaderboard:season:2026 18250 player:841

Absolute updates can simplify recovery and duplicate-event handling because applying the same score twice does not increase it twice.

Top-N and Rank Lookups

The top 100 players can be read directly from the highest-scoring range.

ZREVRANGE leaderboard:season:2026 0 99 WITHSCORES

A player's rank can be retrieved without scanning every lower-ranked player:

ZREVRANK leaderboard:season:2026 player:841

If the returned zero-based rank is 1249, the displayed position is 1250.

The key architectural advantage is that ranking work happens during index maintenance rather than by sorting millions of rows for every read.

Handle Ties Deterministically

A leaderboard needs a clear policy when multiple players have the same score.

Possible rules include:

  • same score means same displayed rank;
  • earlier achievement wins;
  • fewer games played wins;
  • higher secondary metric wins;
  • player ID provides a final deterministic fallback.

Consider:

Player Score Reached Score At
A 10,000 12:01:04
B 10,000 12:03:48

If earlier achievement wins, Player A should remain above Player B.

The tie-breaker should be defined before implementation because it affects the data model. Encoding several ordering dimensions into one floating-point score can eventually create precision problems.

For complicated ranking rules, a custom ranking service or secondary ordered representation may be safer than clever numeric encoding.

Design the Write Path

The write path determines how quickly score changes appear and how safely the ranking store can be rebuilt after failures.

Synchronous Updates

A simple design updates both the durable score and ranking index during the same request.

  1. Game service validates the score change.
  2. Durable player score is updated.
  3. Leaderboard index is updated.
  4. The request completes.

This provides low visibility latency but introduces a dual-write problem. The database update may succeed while the leaderboard update fails.

Attempting to wrap an independent database and Redis cluster in one distributed transaction adds substantial complexity and usually is not justified for leaderboard data.

Event-Driven Updates

A more scalable design publishes score changes as events.

Game Service → Durable Score → Event Stream → Leaderboard Workers → Ranking Store

A score event might contain:

{
    "event_id": "event-9122381",
    "player_id": 841,
    "season_id": 2026,
    "score": 18250,
    "version": 491
}

Leaderboard workers consume these events and update the ranking index asynchronously.

This design decouples game requests from leaderboard latency and allows workers to scale independently when write volume increases.

The cost is eventual consistency. A new score may take hundreds of milliseconds or several seconds to become visible during heavy traffic.

For a leaderboard, that trade-off is often acceptable.

Prevent Duplicate Score Updates

Message brokers can deliver events more than once. A worker may update the leaderboard and crash before acknowledging the message, causing redelivery.

If the event says add 250 points, processing it twice corrupts the score.

A safer event communicates the resulting state:

{
    "player_id": 841,
    "season_id": 2026,
    "score": 18250,
    "version": 491
}

Writing 18250 twice produces the same result.

The version also protects against out-of-order events.

Suppose version 491 sets the score to 18,250, followed later by a delayed version 490 event containing 18,000. Applying events blindly would move the leaderboard backward.

The consumer should only apply an update when its version is newer than the latest processed version for that player.

This is a general distributed-systems principle: prefer idempotent state transitions when messages may be duplicated, delayed, or reordered.

Design the Read Path

Different leaderboard screens require different read patterns, and each should map directly to an efficient ranking operation.

Global Top Players

The most common query is the top N players:

GET /leaderboards/2026?limit=100

The ranking store returns ranks 0 through 99 directly.

This is a highly cacheable response. The global top 100 usually does not need to be recomputed independently for every viewer.

A small cache with a one-second lifetime can turn millions of identical reads into a much smaller number of ranking-store operations while still appearing real-time to users.

Caching strategies and their trade-offs are covered in more detail in Caching Best Practices for Production Systems.

Rank Around a User

A personalized leaderboard often shows the player's immediate neighborhood:

Rank Player Score
1247 Player 92 18,310
1248 Player 443 18,290
1249 Player 841 18,250
1250 Player 317 18,230

The service first retrieves the player's rank and then reads a bounded range around that rank.

rank = ranking_store.get_rank(
    leaderboard="season:2026",
    player_id=841,
)

start = max(0, rank - 5)
end = rank + 5

players = ranking_store.get_range(
    leaderboard="season:2026",
    start=start,
    end=end,
)

This remains efficient even if the player is ranked 8,000,000th because the service does not need to retrieve the preceding 7,999,999 users.

Joining User Metadata

The ranking index should usually contain only the data required for ranking, such as player ID and score.

Usernames, avatars, country, subscription status, and profile information belong in another service or database.

After retrieving 100 player IDs, the leaderboard service can batch-fetch metadata:

Ranking Store → [player IDs] → User Service → Display Records

A common mistake is making one network request per player. Returning the top 100 would then create 100 downstream calls.

Batch APIs or cached profile summaries reduce that fan-out substantially.

Partition the Leaderboard

A single sorted set can handle substantial traffic, but eventually one ranking structure may exceed the memory, throughput, or operational limits of one node.

Leaderboards can be partitioned by dimensions that naturally separate users:

  • region;
  • game mode;
  • season;
  • league;
  • tournament;
  • skill bracket.

For example:

leaderboard:2026:global
leaderboard:2026:us
leaderboard:2026:eu
leaderboard:2026:asia

If regional leaderboards are product requirements anyway, this partitioning is natural.

Arbitrary hash partitioning is harder because ranking is inherently ordered. Hashing players evenly across 20 shards distributes writes nicely, but no shard knows a player's global rank.

This creates one of the key trade-offs in leaderboard design: partitioning by user scales writes, while global ordering requires combining information across partitions.

Global Ranking Across Shards

If a leaderboard is too large or too hot for one ordered index, global ranking requires additional coordination.

One approach partitions players across ranking shards and maintains a smaller global structure containing the strongest candidates from each shard.

Suppose the product only needs the global top 100. Each shard can expose its top 100, and an aggregation service merges those candidate lists.

If there are 100 shards, the aggregator sorts at most 10,000 candidates rather than all 50 million players.

import heapq
from collections.abc import Iterable


def global_top_n(
    shard_results: Iterable[list[tuple[int, int]]],
    limit: int,
) -> list[tuple[int, int]]:
    candidates = (
        (score, player_id)
        for shard in shard_results
        for player_id, score in shard
    )

    top = heapq.nlargest(limit, candidates)

    return [
        (player_id, score)
        for score, player_id in top
    ]

This works well for top-N queries because only a small fraction of each shard matters.

Exact arbitrary global rank is harder. Determining that player 841 is exactly number 7,432,188 requires knowing how many players across all shards have a higher score.

Possible approaches include maintaining score histograms, range-partitioned indexes, periodically computed global rank snapshots, or accepting approximate rank for deep positions.

The requirement should drive the complexity. A product that displays only the top 1,000 plus approximate percentile does not need infrastructure capable of exact global rank for every one of 50 million users.

Seasonal and Segmented Leaderboards

Real products rarely have one permanent global leaderboard.

Common variants include:

  • daily leaderboard;
  • weekly leaderboard;
  • season leaderboard;
  • country leaderboard;
  • friends leaderboard;
  • game-mode leaderboard;
  • tournament leaderboard.

The leaderboard identifier should therefore be part of the storage key rather than hard-coded into the service.

leaderboard:{type}:{scope}:{period}

Examples:

leaderboard:global:all:season-42
leaderboard:country:us:season-42
leaderboard:mode:ranked:2026-09-05

Completed seasons can be made immutable and moved to cheaper storage if interactive rank updates are no longer required.

This reduces memory pressure on the real-time ranking tier and makes retention predictable.

A friend leaderboard creates a different access pattern. Instead of ranking every user globally, the service may retrieve scores for a relatively small set of friend IDs and sort them in application memory.

Not every leaderboard needs the same storage architecture. The global leaderboard and a 50-person friends leaderboard should not automatically use identical query paths.

Failure and Recovery

The ranking store should be treated as rebuildable derived state rather than the only copy of a player's score.

If a ranking node loses data, recovery can proceed from the durable score database or replayable score-event stream.

This makes failure handling substantially safer than relying on the in-memory ranking index as the system of record.

Useful recovery mechanisms include:

  • ranking-store replication;
  • periodic snapshots;
  • durable score records;
  • replayable event streams;
  • background consistency checks;
  • rebuilding one leaderboard without affecting unrelated ones.

If an event consumer falls behind, reads can continue from slightly stale ranking data while the consumer catches up.

Important metrics include consumer lag, ranking update latency, event-processing errors, ranking-store memory utilization, replication lag, rebuild duration, and discrepancy rate between durable scores and ranking entries.

Recovery strategies for asynchronous systems are discussed further in Failure Recovery in Distributed Systems.

What Does Not Scale Well

Several straightforward designs work for small applications but become problematic at leaderboard scale.

Approach Problem Better Approach
Sort the entire database on every request Expensive repeated ranking work Maintain an ordered ranking index
Store leaderboard only in memory Node failure can destroy authoritative score state Keep durable canonical scores
One metadata request per ranked player Creates large downstream fan-out Batch lookup or cache metadata
Apply score increments from duplicate events Redelivery can inflate scores Use idempotent absolute score updates and versions
One global ranking node forever Eventually hits memory or throughput limits Partition by product dimensions or ranking shards
Calculate exact rank for every user continuously Performs unnecessary work for rarely viewed ranks Calculate rank on demand from ordered state

Another common mistake is requiring perfect real-time consistency without a product reason.

If a user earns points and appears at rank 421 instead of 419 for 500 milliseconds, almost nothing is lost. Forcing every score update through globally synchronous coordination to avoid that temporary discrepancy can severely reduce availability and throughput.

Production Design

A practical design for millions of users can separate durable scoring, asynchronous propagation, and read-optimized ranking.

  1. The game service validates an action that changes a player's score.
  2. The authoritative database stores the new absolute score and monotonically increasing version.
  3. A durable score-change event is published.
  4. Leaderboard workers consume events independently of the game request path.
  5. The worker ignores duplicate or older versions.
  6. The ranking store updates the player's score in the appropriate sorted structure.
  7. Top-N requests read directly from the ordered index.
  8. Popular top-N responses are cached for a very short period.
  9. Personalized requests retrieve the player's rank and a bounded range around it.
  10. Player metadata is fetched in batches or from cache.
  11. Large leaderboards are partitioned when one ranking node no longer meets memory or throughput targets.
  12. The ranking tier can be rebuilt from durable scores or replayed events.

Important production metrics include:

  • score updates per second;
  • leaderboard reads per second;
  • p50, p95, and p99 rank-query latency;
  • score-to-leaderboard propagation latency;
  • event consumer lag;
  • ranking-store CPU and memory utilization;
  • cache hit ratio for popular leaderboard pages;
  • duplicate and stale events rejected;
  • ranking reconciliation mismatches;
  • hot leaderboard or shard traffic.

The most important service-level metric is often score visibility latency: how long it takes from accepting a score update until the correct value appears in leaderboard reads.

A leaderboard can have excellent API latency while still providing a poor experience if ranking consumers are several minutes behind.

Monitoring latency, saturation, queues, and asynchronous pipelines is covered more broadly in Observability Best Practices for Production Systems.

How to Answer This in a System Design Interview

A strong interview answer should first identify the core access pattern rather than immediately naming Redis.

The system needs an ordered index that supports frequent score updates, top-N reads, and direct rank lookup. Sorting millions of database rows per request is unnecessary, so durable score storage should be separated from a read-optimized ranking structure.

Then build the design in layers.

  1. Clarify the product semantics. Define scale, update rate, top-N queries, rank lookup, tie-breaking, seasons, and acceptable ranking delay.
  2. Keep canonical scores durable. Store authoritative player scores in a persistent database.
  3. Use an ordered ranking index. Redis sorted sets or another ordered data structure provide efficient score updates and rank-range queries.
  4. Decouple heavy write traffic. Publish score events and let leaderboard consumers update ranking state asynchronously.
  5. Make events idempotent. Send absolute scores with versions rather than blindly applying duplicate increments.
  6. Optimize common reads. Read top-N and rank neighborhoods directly from the ranking structure and cache highly popular results briefly.
  7. Avoid metadata fan-out. Batch or cache user profile information.
  8. Partition when necessary. Split by season, region, mode, or shards when one ordered structure reaches its limits.
  9. Discuss global-rank complexity. Top-N can be merged efficiently across shards, while exact arbitrary global rank requires more coordination.
  10. Design for rebuildability. Treat the ranking index as derived state that can be recovered from durable scores or event history.

The key system-design insight is that the leaderboard is a materialized ordered view of score data. The durable database protects correctness, while a specialized ranking structure provides the low-latency access patterns required by the product.

Conclusion

A real-time leaderboard for millions of users should not repeatedly sort millions of database rows. The system should maintain an ordered ranking index that supports efficient updates, top-N reads, individual rank lookup, and bounded rank ranges.

Durable score storage and asynchronous score events provide reliability and rebuildability, while structures such as Redis sorted sets provide low-latency ranking. Versioned absolute score updates prevent duplicated or reordered messages from corrupting results.

At larger scale, the main trade-off becomes global ordering versus partitioning. Top-N queries can be merged efficiently across shards, while exact rank for arbitrary users requires more coordination. The best design provides only as much global consistency and ranking precision as the product actually needs.

Comments (0)