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

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