Leaderboard System Low-Level Design

What is a Leaderboard System?

A leaderboard ranks users or entities by a score, updated in real time. Use cases: gaming (top players by score), e-commerce (top sellers this month), fitness apps (weekly step count), developer platforms (contributions on GitHub). The core challenge is serving ranked queries efficiently when scores update continuously at high volume.

Requirements

  • Update user score (increment or set) with sub-10ms latency
  • Query top-N (e.g., top 100) users across the global leaderboard: <20ms
  • Query a user’s current rank: <20ms
  • Support time-windowed leaderboards: all-time, weekly, daily
  • 10M active users, 50K score updates/second during peak events
  • Leaderboard page pagination: get users ranked 201-300

Core Data Structure: Redis Sorted Set

Redis Sorted Set (ZSET) is the right tool. It maintains members sorted by score with O(log N) updates and O(log N + k) range queries.

Key operations:
ZADD leaderboard:global {score} {user_id}   # set/update score
ZINCRBY leaderboard:global {delta} {user_id} # increment score
ZREVRANK leaderboard:global {user_id}        # 0-based rank (0 = #1)
ZREVRANGE leaderboard:global 0 99 WITHSCORES # top 100
ZREVRANGEBYSCORE leaderboard:global +inf -inf LIMIT 200 100  # page 3 (rank 201-300)
ZCARD leaderboard:global                     # total users on leaderboard

Time-Windowed Leaderboards

Weekly and daily leaderboards require separate sorted sets per window. Key naming:

leaderboard:global          -- all-time
leaderboard:weekly:2026-W15 -- week 15 of 2026
leaderboard:daily:2026-04-12 -- specific day

Score update applies to all relevant windows:

def add_score(user_id, delta):
    pipe = redis.pipeline()
    pipe.zincrby('leaderboard:global', delta, user_id)
    pipe.zincrby(f'leaderboard:weekly:{current_week()}', delta, user_id)
    pipe.zincrby(f'leaderboard:daily:{today()}', delta, user_id)
    pipe.execute()  # atomic pipeline

Expiry: set TTL on windowed keys so old leaderboards are automatically cleaned up:

redis.expire(f'leaderboard:daily:{today()}', 86400 * 8)   # keep 8 days
redis.expire(f'leaderboard:weekly:{current_week()}', 86400 * 60)  # keep 60 days

Persistent Storage

Redis holds the live leaderboard in memory. For persistence and analytics, write score events to a relational DB:

ScoreEvent(event_id UUID, user_id UUID, delta INT, new_score BIGINT,
           source VARCHAR, created_at TIMESTAMP)

UserScore(user_id UUID, all_time_score BIGINT, weekly_score BIGINT,
          daily_score BIGINT, last_updated TIMESTAMP)

On score update: write to Redis (synchronous, low latency) + publish to Kafka → async consumer writes ScoreEvent to DB. Redis is the source of truth for live rankings; DB is for analytics, auditability, and Redis recovery.

User Rank Query

def get_user_rank(user_id, window='global'):
    key = get_key(window)
    rank = redis.zrevrank(key, user_id)  # 0-indexed
    if rank is None:
        return None  # user not on leaderboard
    score = redis.zscore(key, user_id)
    total = redis.zcard(key)
    return {'rank': rank + 1, 'score': score, 'total_users': total,
            'percentile': round((1 - rank/total) * 100, 1)}

Leaderboard Page with User Details

ZREVRANGE returns user IDs and scores, but the client needs display names and avatars. Avoid N+1 queries:

def get_leaderboard_page(offset, limit, window='global'):
    key = get_key(window)
    entries = redis.zrevrange(key, offset, offset+limit-1, withscores=True)
    # entries = [('user123', 5000.0), ('user456', 4800.0), ...]

    user_ids = [uid for uid, _ in entries]
    users = get_users_batch(user_ids)  # single DB query with IN clause

    return [{'rank': offset+i+1, 'user': users[uid], 'score': int(score)}
            for i, (uid, score) in enumerate(entries)]

Key Design Decisions

  • Redis ZSET for rankings — O(log N) updates and range queries, native rank/score operations
  • Pipeline for multi-window updates — atomic, single round-trip for global+weekly+daily
  • TTL on windowed keys — automatic cleanup, no cron jobs needed
  • Kafka for async persistence — score updates are low latency; DB write happens asynchronously
  • Batch user lookup — avoids N+1 when hydrating leaderboard page with user details

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”What data structure should I use for a real-time leaderboard?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use a Redis Sorted Set (ZSET). It maintains members ordered by score with O(log N) updates via ZADD/ZINCRBY and O(log N + k) range queries via ZREVRANGE. For 10M users, Redis ZSET handles 50K score updates/second with sub-millisecond latency — no SQL database can match this for ranked queries.”}},{“@type”:”Question”,”name”:”How do you implement time-windowed leaderboards (daily/weekly)?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use separate Redis sorted set keys per time window: leaderboard:global, leaderboard:weekly:2026-W15, leaderboard:daily:2026-04-12. On each score update, use a Redis pipeline to atomically increment all three keys in one round trip. Set TTL on windowed keys (e.g., 8 days for daily) so they expire automatically without a cleanup job.”}},{“@type”:”Question”,”name”:”How do you get a user’s rank efficiently?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use ZREVRANK key user_id — O(log N), returns the 0-based rank (add 1 for 1-indexed display). For context, also fetch total users via ZCARD. The result is instantaneous with no full scan needed. If the user is not in the sorted set, ZREVRANK returns nil — handle this case to show "unranked".”}},{“@type”:”Question”,”name”:”How do you paginate through a leaderboard?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use ZREVRANGEBYSCORE or ZREVRANGE with offset and limit: ZREVRANGE leaderboard:global 200 299 WITHSCORES returns ranks 201-300 (0-indexed offset=200, count=100). Then batch-fetch user display data (names, avatars) with a single DB query using IN clause — never make N separate DB calls for each leaderboard entry.”}},{“@type”:”Question”,”name”:”How do you persist leaderboard data beyond Redis?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Write score update events to Kafka asynchronously. A consumer writes ScoreEvent records to a data warehouse for analytics and auditability. Redis is the source of truth for live rankings. If Redis is lost, rebuild the sorted set by replaying ScoreEvents from the DB or from a periodic snapshot. Redis AOF/RDB persistence reduces the recovery window.”}}]}

Real-time leaderboard design is a common topic in Meta interview questions on system design.

Redis sorted sets for rankings are discussed in Google system design interview preparation.

Leaderboard and ranking systems appear in Amazon system design interview guide.

Scroll to Top