See It Flow
Real-Time Gaming Leaderboard System Design

Real-Time Leaderboard Architecture — Ranking Algorithms and Data Structures

This page assumes the problem framing, requirements, and high-level architecture from Complete Design — Arena Clash (arena-clash), mode solo, season S7, scope global — and goes straight into mechanism: the comparator that defines rank, how a sorted set actually represents that order, the exact pseudocode for each ranking policy's score update, the idempotent ingestion path that makes every algorithm below safe to retry, and the query, sharding, live-update, season-lifecycle, moderation, and recovery machinery built on top of it. Every numbered test (A–K) below is the same canonical test vector used across the rest of this module, traced through the actual algorithm rather than just asserted.

Mangesh Mandlik·Updated September 2026
Component 01

Ranking Semantics and the Deterministic Comparator

Every ranking decision a leaderboard ever makes — a ZSET's internal order, a database ORDER BY, a pagination window, a push notification saying "you moved up" — must agree with exactly one function: the comparator. If two parts of the system can disagree about whether player X outranks player Y, the leaderboard has two sources of truth, and that is the bug class this entire page exists to prevent.

Three ranking policies, three sort directions

Arena Clash's global solo board for season S7 ranks by highest verified score wins — descending. A separate cumulative-mode board (Test J) ranks by highest accumulated total — also descending, but the update rule that produces the total is fundamentally different from a single best-score field (Component 06). A separate fastest-time board (Test K) ranks by lowest elapsed time wins — ascending, the one case in this page where "better" means a smaller number. These are not three variations of one sort; they are three distinct comparators with three distinct update algorithms, and treating them as interchangeable is the single most common implementation mistake in this domain.

#1Ava9,800 pts#2Ben9,500 pts#3Chloe9,500 pts#4Dev9,100 pts#5Eli8,700 pts
Arena Clash S7 global solo board in its canonical initial state — five players, one deterministic order

Deterministic unique order, not competition or dense ranking

This design produces a strict total order — rank 1, 2, 3, 4, 5 — never competition ranking (1, 1, 3, where two tied players share rank 1 and rank 2 is skipped) and never dense ranking (1, 1, 2, where no rank is skipped). Every player occupies exactly one rank in exactly one position, because ties are always broken by a deterministic secondary and tertiary key (Component 03) before a rank number is ever assigned. A leaderboard that reports "two players tied for #2" either hasn't applied its tie-break yet, or has chosen competition ranking as an explicit product decision — Arena Clash's S7 global board has not; every test vector in this module assumes unique, ordered ranks.

The comparator is the contract

At the top level, every ranking policy reduces to one function signature: given two candidate rows, return which one ranks first. The body of that function differs per policy (Components 05–07 give the full, concrete versions), but its shape is always the same three-step shape, and every other piece of this architecture — the ZSET score encoding, the SQL ORDER BY, the nearby-rank window logic — must be provably equivalent to calling this function pairwise, not merely "usually produce the same result."

pseudocodeThe top-level comparator shape every ranking policy implements — direction and tie-break are the only two things that vary
function compareForRank(a, b, policy):
    // 1. Primary ranking value, direction depends on policy
    if policy.direction == DESCENDING:       // best-score, cumulative
        if a.value != b.value:
            return (a.value > b.value) ? A_FIRST : B_FIRST
    else:                                     // fastest-time
        if a.value != b.value:
            return (a.value < b.value) ? A_FIRST : B_FIRST

    // 2. Tie-break: policy-specific, see Component 03 (score policies)
    //    or Component 07 (fastest-time's own, separately-defined tie policy)
    return policy.tieBreak(a, b)
Key takeaways
  • Best-score and cumulative boards sort descending; fastest-time sorts ascending — these are different comparators, not the same one with a flipped flag applied carelessly
  • Ranks are a strict total order (1, 2, 3, ...) via deterministic tie-break, never competition (1, 1, 3) or dense (1, 1, 2) ranking
  • Every component that produces an order — ZSET, SQL query, pagination window — must be provably equivalent to the comparator, not just usually consistent with it
  • A leaderboard row only has meaning within one fixed identity: game + mode + season + scope
Component 02

Sorted Sets Internally: Ordered Structure Plus Hash Lookup

A Redis-style sorted set is really two cooperating structures behind one API: a skip-list/balanced-tree-style ordered structure keyed by score that supports fast range and rank queries, and a hash map from member to score that supports O(1) "what is this player's current score" lookups without any traversal.

Ordered structure intuition

Conceptually, think of the score-ordered side as a structure that keeps elements sorted by (score, member) and lets you walk forward or backward from any point, jump to "the element at position k," or ask "how many elements come before this one" — all without rescanning everything. A skip list (multiple linked layers, each layer skipping more elements than the one below) and a balanced binary search tree are two concrete ways to implement that intuition; which one a given data store actually uses internally is an implementation detail of that store and version, not something this page verifies or depends on.

arena-clash:solo:S7:global (ZSET)ZREVRANK 0p-ava9800ZREVRANK 1p-ben9500ZREVRANK 2p-chloe9500ZREVRANK 3p-dev9100ZREVRANK 4p-eli8700
The arena-clash:solo:S7:global sorted set: member → score, ordered for ZREVRANK / ZREVRANGE

Complexity, conceptually — not a measured benchmark

OperationConceptual complexityWhy
Score lookup by member (ZSCORE)~O(1)Direct hash-map lookup from member to score, no traversal of the ordered structure
Insert / update a member (ZADD)~O(log N)Ordered structure must find and maintain this member's position relative to N other members
Rank of a member (ZREVRANK)~O(log N)Walking the ordered structure while counting elements passed, rather than scanning linearly
Top-N range (ZREVRANGE)~O(log N + k)Locate the starting point in ~O(log N), then walk k elements forward
Remove a member (ZREM)~O(log N)Symmetric to insert — remove from both the ordered structure and the hash map
Conceptual complexity of core sorted-set operations (illustrative, not verified internals of any specific version of any specific data store)

Real commands against the canonical board

Loading the canonical initial state and reading it back with real sorted-set commands immediately surfaces the problem Component 03 exists to solve.

textZADD / ZREVRANGE / ZREVRANK / ZSCORE against the canonical Arena Clash board. Equal scores default to reverse-lexicographic member order (`p-chloe` before `p-ben`) — the opposite of the required rank (Ben #2, Chloe #3).
ZADD leaderboard:arena-clash:solo:S7:global 9800 p-ava
ZADD leaderboard:arena-clash:solo:S7:global 9500 p-ben
ZADD leaderboard:arena-clash:solo:S7:global 9500 p-chloe
ZADD leaderboard:arena-clash:solo:S7:global 9100 p-dev
ZADD leaderboard:arena-clash:solo:S7:global 8700 p-eli

ZREVRANGE leaderboard:arena-clash:solo:S7:global 0 4 WITHSCORES
# 1) p-ava    9800
# 2) p-chloe  9500   <-- native order puts Chloe ahead of Ben
# 3) p-ben    9500   <-- even though Ben achieved 9,500 one minute earlier
# 4) p-dev    9100
# 5) p-eli    8700

ZREVRANK leaderboard:arena-clash:solo:S7:global p-ben
# => 2   (0-indexed; "rank 3" in human terms, from the native tie order)

ZSCORE leaderboard:arena-clash:solo:S7:global p-dev
# => "9100"
Key takeaways
  • A sorted set is an ordered structure (skip-list/tree-style intuition) plus a hash map from member to score — the second structure is what makes ZSCORE O(1)
  • Complexity figures here are conceptual (~O(log N)), explicitly not a verified benchmark of any specific store or version
  • ZADD / ZREVRANGE / ZREVRANK / ZSCORE are real commands — run against the canonical board, they expose exactly the tie-break gap this page addresses next
  • Never treat a sorted set's native equal-score ordering as implementing a product's actual tie-break rule
Component 03

Tie-Breaking Implementation

Arena Clash's tie-break rule is: same score → earlier achievement timestamp wins → if still tied, lower stable player ID wins. None of that lives in a sorted set's native comparison, so it has to be implemented explicitly, either by encoding it into the score or by resolving it against a secondary field whenever two rows tie.

1. Compare score2. If equal → earlier achievedAt wins3. If still equal → lower stable player ID wins
The three-step tie comparator: score, then achieved-at timestamp, then stable player ID

The concrete tie comparator

pseudocodeDeterministic tie comparator for best-score and cumulative policies: achieved-at ascending, then player ID ascending
function tieBreakScorePolicy(a, b):
    // called only when a.value == b.value
    if a.achievedAt != b.achievedAt:
        return (a.achievedAt < b.achievedAt) ? A_FIRST : B_FIRST   // earlier timestamp wins
    // still tied (should be rare but must still be deterministic)
    return (a.playerId < b.playerId) ? A_FIRST : B_FIRST            // lower stable ID wins

Test B, traced: Eli ties Ben and Chloe at 9,500

From the original state, Eli reaches a verified 9,500 at 10:06:00. Three players now share the value 9,500: Ben (achieved 10:01:00), Chloe (achieved 10:02:00), Eli (achieved 10:06:00). Applying the tie comparator pairwise: Ben's 10:01:00 is earlier than both Chloe's and Eli's, so Ben stays #2. Chloe's 10:02:00 is earlier than Eli's 10:06:00, so Chloe stays #3. Eli, with the latest achievement timestamp among the tied group, lands at #4, pushing Dev to #5. The final order is Ava (#1), Ben (#2), Chloe (#3), Eli (#4), Dev (#5) — exactly the ordering a correct implementation of the comparator above produces, and exactly the opposite of what native reverse-lexicographic order would have given for the Ben/Chloe pair.

Composite-score encoding: tempting, and a real precision pitfall

One common approach is to encode the tie-break directly into the numeric score so that native sorted-set order already matches the desired order — for example, computing encodedScore = rawScore * 10^13 - achievedAtEpochMillis so that, for equal rawScore, an earlier achievedAtEpochMillis produces a larger encodedScore. This can work, but it trades a correctness problem for a precision problem: scores are commonly stored as IEEE-754 double-precision floats, which exactly represent integers only up to 2^53 (about 9×10^15). A raw score of up to 7 digits combined with a 13-digit millisecond epoch timestamp can require roughly 20 significant digits once multiplied together — comfortably past the point where two distinct encoded values can silently collapse into the same floating-point representation, reintroducing exactly the ambiguous-order problem this encoding was supposed to solve, except now hidden inside an opaque number instead of an obvious missing tie-break.

Key takeaways
  • Tie-break order is score, then earlier achieved-at timestamp, then lower stable player ID — implemented explicitly, never assumed from native ordering
  • Test B: Ben (10:01) stays #2, Chloe (10:02) stays #3, Eli (10:06, latest) lands #4, Dev drops to #5
  • Composite score encoding (packing a timestamp into the score) can exceed floating-point integer precision at realistic score/timestamp magnitudes — a real pitfall, not a theoretical one
  • A safer default: keep the stored score pure, and resolve ties against a secondary achieved-at/player-ID field pulled from the authoritative source
Component 04

Authoritative Score State and Conditional Writes

The sorted set is a fast, queryable projection — the authoritative truth about what a player's best score is, when they achieved it, and which event proved it lives in a row-oriented aggregate store that every write must pass through conditionally, never unconditionally.

FieldTypePurpose
leaderboard_idstring (game, mode, season, scope)Composite identity this row belongs to — e.g. arena-clash / solo / S7 / global
player_idstring, stableThe tie-break-of-last-resort key; never reassigned
valueintegerBest score, cumulative total, or best time in milliseconds, depending on the board's policy — never a float
achieved_attimestamp (UTC, event-time)When the current value was actually earned — the tie-break field, and the field replay must reproduce exactly
versioninteger, monotonically increasingOptimistic-concurrency counter; incremented on every accepted write to this row
last_event_idstringThe unique event ID that produced the current value — the anchor for idempotent replay and audit
statusenum: active, invalidated, pending_reviewModeration state (Component 17) — invalidated/pending rows are excluded from ranking
updated_attimestamp (UTC, processing-time)Bookkeeping only — audit and latency measurement, never an input to ranking order
Core data model: one row per (player, leaderboard identity)
Game Serververified eventLeaderboard APIScores DBORDER BY score DESCsubmitUPSERT / SELECT
Game server submits a verified event; the leaderboard API applies it to the authoritative scores store before anything is projected into Redis

Conditional highest-score update

The write that actually matters is a single conditional statement: update only if the new score is strictly better, and only if no concurrent writer has already advanced this row's version out from under us.

sqlConditional highest-score update: monotonic guard and optimistic-concurrency version check in one statement
BEGIN;

UPDATE player_scores
SET value         = :new_value,
    achieved_at   = :new_achieved_at,
    version       = version + 1,
    last_event_id = :event_id
WHERE player_id    = :player_id
  AND leaderboard_id = :leaderboard_id
  AND version       = :expected_version   -- optimistic-concurrency guard
  AND :new_value    > value;              -- monotonic guard: never overwrite with a worse value

-- rowCount == 0 means one of two distinct things, not one generic failure:
--   a) a concurrent writer already advanced `version` past :expected_version
--      (optimistic-lock conflict -> re-read current row and retry)
--   b) :new_value was not actually better than the current value
--      (correct, intentional no-op -- not an error, not a retry target)

COMMIT;
pseudocodeApplication-level retry loop around the conditional update — re-reads on a version conflict instead of assuming the first read is still valid
function applyWithOptimisticRetry(playerId, leaderboardId, candidateValue, candidateAchievedAt, eventId):
    for attempt in 1..MAX_RETRIES:
        current = readRow(playerId, leaderboardId)
        if not isStrictlyBetter(candidateValue, current.value, policy):
            return Result.NO_OP                      // weaker submission, not an error

        rowsAffected = conditionalUpdate(playerId, leaderboardId,
                                          expectedVersion = current.version,
                                          newValue = candidateValue,
                                          newAchievedAt = candidateAchievedAt,
                                          eventId = eventId)
        if rowsAffected == 1:
            return Result.APPLIED
        // rowsAffected == 0: version moved under us -- re-read and retry, never blind-overwrite
    return Result.RETRIES_EXHAUSTED

Transaction boundaries

The single UPDATE above is already atomic on its own. The moment a write needs to touch more than one thing — for example, updating player_scores and appending a row to an event-history/audit table in the same logical operation — both statements belong inside one transaction, committed or rolled back together. A design that updates the score row, then separately appends the audit row as an unrelated follow-up write, creates a window where a crash between the two leaves the authoritative row updated but with no corresponding audit trail — exactly the kind of partial-application state that replay and disaster recovery (Component 18) depend on never happening.

Key takeaways
  • The authoritative row carries value, achieved_at, version, last_event_id, and status — the sorted set never has to be the system of record for any of these
  • The conditional UPDATE combines a monotonic guard (new value strictly better) and an optimistic-concurrency check (version match) in one statement
  • Zero rows affected has two distinct causes — version conflict (re-read and retry) vs. a genuinely weaker submission (correct no-op) — and conflating them is a bug
  • Any write touching more than one table belongs in one transaction; partial application across separate writes breaks the replay guarantee this design depends on
Component 05

Best-Score Algorithm: Monotonic Updates

The best-score policy's entire correctness rests on one rule: a submitted score only ever replaces the stored value if it is strictly higher, applied exactly once per unique event, regardless of when that event happens to arrive relative to others.

pseudocodeBest-score update: dedup check, monotonic comparison, then the conditional write from Component 04
function applyBestScoreEvent(event):                 // event: { eventId, playerId, leaderboardId, score, achievedAt }
    if dedupStore.hasSeen(event.eventId):             // Component 08 — checked before anything else
        return Outcome.DUPLICATE_IGNORED

    current = readRow(event.playerId, event.leaderboardId)

    if current == null:
        insertRow(event.playerId, event.leaderboardId,
                  value = event.score, achievedAt = event.achievedAt,
                  version = 1, lastEventId = event.eventId)
        dedupStore.markSeen(event.eventId)
        return Outcome.APPLIED_FIRST_SCORE

    if event.score <= current.value:
        dedupStore.markSeen(event.eventId)             // still record it: a reprocessed retry must stay a no-op
        return Outcome.REJECTED_NOT_AN_IMPROVEMENT

    applyWithOptimisticRetry(event.playerId, event.leaderboardId,
                              candidateValue = event.score,
                              candidateAchievedAt = event.achievedAt,
                              eventId = event.eventId)
    dedupStore.markSeen(event.eventId)
    return Outcome.APPLIED_NEW_BEST
BEFOREAFTER evt-101 (9,700)#1 Ava 9,800#2 Ben 9,500#3 Chloe 9,500#4 Dev 9,100#5 Eli 8,700#1 Ava 9,800#2 Dev 9,700#3 Ben 9,500#4 Chloe 9,500#5 Eli 8,700#4 → #2
Dev's verified 9,700 (evt-101) moves him from #4 to #2 — Ava and the 9,500 tier are untouched

Test A, traced

Dev submits a verified 9,700 at 10:05:00 as evt-101. dedupStore.hasSeen('evt-101') is false, so processing continues. Dev's current row has value = 9,100. Since 9,700 > 9,100, this is a genuine improvement: the conditional update sets value = 9,700, achieved_at = 10:05:00, version += 1, last_event_id = 'evt-101', and evt-101 is marked seen. Re-ranking the board now places Dev between Ava and the 9,500 tier: Ava 9,800 (#1), Dev 9,700 (#2), Ben 9,500 (#3), Chloe 9,500 (#4), Eli 8,700 (#5) — none of the other four rows were ever touched; only Dev's row changed, and the new global order falls out purely from the comparator being re-evaluated.

Test C, traced: reprocessing evt-101 is a true no-op

If evt-101 is redelivered after Test A — a retry, a replayed message, a consumer that crashed after processing but before acknowledging — dedupStore.hasSeen('evt-101') is now true, and the function returns Outcome.DUPLICATE_IGNORED before even reading the current row. Dev's row, the board order, and every other piece of state are completely unaffected. This is what "zero additional effect" means concretely: the second delivery never even reaches the monotonic comparison.

Test D, traced: a late, weaker submission is rejected on magnitude alone

After Test A, a delayed Dev event carrying a score of 9,200 arrives as a distinct event (not evt-101 — say evt-099, delayed in transit). It passes the dedup check (new event ID, never seen). readRow returns Dev's current value of 9,700. Since 9,200 <= 9,700, this is Outcome.REJECTED_NOT_AN_IMPROVEMENT — a correct, intentional no-op. Notice this rejection needed no reasoning about *when* the event was generated relative to evt-101; the monotonic guard compares magnitude only, which is exactly correct for a best-score policy. (Component 09 covers the case where event-time ordering of the achieved_at field itself matters, which is a separate concern from whether the update applies at all.)

Key takeaways
  • Best-score update order: dedup check first, then monotonic comparison, then the conditional write — in that order, never reordered
  • Test A: Dev's 9,700 (evt-101) is strictly better than his 9,100, so it applies and he re-ranks to #2
  • Test C: redelivering evt-101 is caught by the dedup check before the monotonic comparison even runs — truly zero effect
  • Test D: a late, weaker 9,200 submission is rejected purely on magnitude (9,200 ≤ 9,700) — arrival timing is irrelevant to whether a best-score update applies
Component 06

Cumulative-Score Algorithm: Idempotent Increments

A cumulative board — a separate leaderboard identity from the best-score board, same comparator shape, different update semantics — adds a delta to a running total on every event instead of comparing against a stored best. That single difference changes everything about how idempotency has to be enforced, because unlike a monotonic best-score guard, there is no magnitude check that can reject a duplicate on its own: adding the same +50 twice is indistinguishable from two legitimate +50 events unless the event IDs are tracked explicitly.

pseudocodeCumulative update: the increment and the dedup mark commit atomically together — not as two separately-failable steps
function applyCumulativeEvent(event):                 // event: { eventId, playerId, leaderboardId, delta, achievedAt }
    if dedupStore.hasSeen(event.eventId):
        return Outcome.DUPLICATE_IGNORED               // the ONLY thing preventing a double-count

    beginTransaction()
    current = readRowForUpdate(event.playerId, event.leaderboardId)   // row lock / SELECT ... FOR UPDATE
    newTotal = (current == null ? 0 : current.value) + event.delta
    upsertRow(event.playerId, event.leaderboardId,
              value = newTotal, achievedAt = event.achievedAt,
              version = (current == null ? 1 : current.version + 1),
              lastEventId = event.eventId)
    dedupStore.markSeen(event.eventId)                  // dedup-mark happens in the SAME transaction
    commitTransaction()
    return Outcome.APPLIED
textZINCRBY has no concept of event identity — calling it twice for the same event always adds twice. The dedup gate must sit in front of this call, never behind it.
ZINCRBY leaderboard:arena-clash:cumulative:S7:global 100 p-dev
# => "100"
ZINCRBY leaderboard:arena-clash:cumulative:S7:global 50 p-dev
# => "150"
ZINCRBY leaderboard:arena-clash:cumulative:S7:global 50 p-dev   # the SAME event replayed
# => "200"   <-- WRONG if this call is reached again for an event already applied

Test J, traced: 100 + 50 + duplicate 50 = 150, not 200

Event 1 (evt-c1, delta +100): not seen before, applies — total becomes 100. Event 2 (evt-c2, delta +50): not seen before, applies — total becomes 150. A duplicate delivery of the *same* evt-c2 arrives again (same event ID, not a new +50 event): dedupStore.hasSeen('evt-c2') is now true, so it is ignored before the increment ever runs. Final total: 150. Had the system instead deduplicated by delta value or by player+timestamp fuzzy-matching instead of a strict unique event ID, a legitimately distinct second +50 event (different real-world action, different event ID) would have been wrongly discarded too — which is exactly why the dedup key is the event ID alone, not any derived property of the event's contents.

Corrections on a cumulative board

If a previously-applied delta needs to be reversed — a moderation decision invalidates one scoring event after the fact — the fix is never to directly edit value downward in place. It is to apply an explicit, audited correction event carrying a negative delta (and its own unique event ID), run through the exact same applyCumulativeEvent path. This keeps the full history replayable: summing every event's delta in event order, including corrections, always reconstructs the same final total, which a silent in-place edit would break (Component 18 depends on this).

Key takeaways
  • Cumulative totals have no self-defending monotonic guard — the unique-event-ID dedup check is the only thing preventing a double-count
  • The increment and the dedup mark must commit in one transaction; applying one without the other reopens the double-count or lost-update window
  • Test J: +100, +50, then a duplicate delivery of the same +50 event (same event ID) → total stays 150, never 200
  • Corrections to a cumulative total are applied as new, signed, audited delta events — never a direct in-place edit of the stored total
Component 07

Fastest-Time Algorithm: Ascending Order and Its Own Tie Policy

A fastest-time board — a third, separate leaderboard identity — flips the comparator's primary direction (lower is better) and needs its own explicit tie policy; it must never silently inherit the descending score comparator and its tie-break by accident, because applying a "highest wins" tie-break to a "lowest wins" board produces a result that looks plausible but is simply wrong.

Units and precision: integer milliseconds, not floating-point seconds

Elapsed time is stored as an integer count of milliseconds (52340, not 52.34), for the same reason prices and quantities are stored as integers elsewhere in well-built systems: floating-point subtraction and comparison can introduce rounding error that makes two times that should compare equal (or unequal) do the opposite. A run that truly finished in 52.340 seconds is stored as the integer 52340; comparisons, sums, and tie-checks all operate on that integer.

pseudocodeFastest-time update: identical shape to best-score, but every comparison direction is explicitly reversed, not inherited
function applyFastestTimeEvent(event):                // event: { eventId, playerId, leaderboardId, timeMs, achievedAt }
    if dedupStore.hasSeen(event.eventId):
        return Outcome.DUPLICATE_IGNORED

    if event.timeMs <= 0 or event.timeMs > MAX_PLAUSIBLE_TIME_MS:
        return Outcome.REJECTED_INVALID_TIMING         // sanity bound, independent of anti-cheat scoring (Component 17)

    current = readRow(event.playerId, event.leaderboardId)

    if current != null and event.timeMs >= current.value:
        dedupStore.markSeen(event.eventId)
        return Outcome.REJECTED_NOT_FASTER              // lower is better here -- not a bug, the comparator is reversed

    applyWithOptimisticRetry(event.playerId, event.leaderboardId,
                              candidateValue = event.timeMs,
                              candidateAchievedAt = event.achievedAt,
                              eventId = event.eventId,
                              isBetter = (candidate, stored) -> candidate < stored)   // explicit ascending comparator
    dedupStore.markSeen(event.eventId)
    return Outcome.APPLIED_NEW_BEST_TIME
pseudocodeFastest-time's own explicit tie comparator — structurally similar to Component 03's, but a distinct function, declared separately on purpose
function tieBreakFastestTimePolicy(a, b):
    // called only when a.timeMs == b.timeMs -- this board's OWN tie policy,
    // not a reuse of tieBreakScorePolicy from Component 03
    if a.submittedAt != b.submittedAt:
        return (a.submittedAt < b.submittedAt) ? A_FIRST : B_FIRST   // earlier submission wins
    return (a.playerId < b.playerId) ? A_FIRST : B_FIRST
textTest K: two illustrative runs tie at 52,340 ms — ZRANGE/ZRANK (ascending) are the correct commands for this board, not ZREVRANGE/ZREVRANK
ZADD leaderboard:arena-clash:time-trial:S7:global 52340 p-fin
ZADD leaderboard:arena-clash:time-trial:S7:global 52340 p-gus

# Ascending order: ZRANGE (not ZREVRANGE), ZRANK (not ZREVRANK) -- lower score first
ZRANGE leaderboard:arena-clash:time-trial:S7:global 0 1 WITHSCORES
# 1) p-fin   52340
# 2) p-gus   52340
ZRANK leaderboard:arena-clash:time-trial:S7:global p-gus
# => 1   (0-indexed; "rank 2" -- native lex order happens to agree here, but still must not be trusted blindly)

Test K, traced

Two illustrative runs — p-fin and p-gus — both finish at exactly 52,340 ms. Neither is faster by the primary comparator, so the board's own tie policy applies: p-fin submitted at 10:10:02, p-gus at 10:10:07. Since p-fin's submission is earlier, p-fin ranks first, p-gus second. Nothing about this trace uses the descending-score comparator or its achieved-at-ascending/player-ID-ascending tie-break from Component 03 — it uses this board's own ascending primary comparison and its own, separately-declared tie function, even though the tie function happens to look structurally similar (earlier-wins, then player-ID-wins).

Key takeaways
  • Elapsed time is stored as an integer number of milliseconds — never a float — to keep comparisons and tie-checks exact
  • The fastest-time comparator explicitly reverses both the primary comparison (lower wins) and is backed by its own, separately-declared tie policy
  • Test K: two runs tie at 52,340 ms; the earlier submission (p-fin) wins, decided by this board's own tie function, not a reused descending one
  • Ascending boards use ZRANGE/ZRANK, not ZREVRANGE/ZREVRANK — mixing these up silently inverts the displayed order
Component 08

Idempotent Ingestion Pipeline

Every algorithm in Components 05–07 assumes a dedup check already ran and can be trusted — this component is where that assumption is actually earned: authenticated intake, a durable append before acknowledgment, and a dedup-store check that is checked before any score logic runs at all.

Game ServerGatewayDedup Storeseen: evt-101evt-101 (1st)evt-101 (retry)1st: processed, recordedretry: recognized, ignored
evt-101 processed and recorded on first delivery; a retried redelivery of evt-101 is recognized and ignored, never reprocessed

Authentication: the game client never writes scores directly

No score event reaches the dedup check or the update algorithms without first being attributable to an authenticated, trusted game-server session — never a bare claim embedded in a message from the player's own client. This is the first and most important anti-cheat boundary (Component 17 builds on it): the ingestion pipeline simply never considers a client-asserted score a candidate event in the first place.

Durable append before acknowledgment

Once authenticated, an event is appended to a durable, ordered event log before the gateway acknowledges receipt back to the game server. This ordering matters: if the gateway acknowledged first and the append failed afterward, a game server that reasonably assumes "acknowledged means durably recorded" would never retry a lost event, and that event is gone forever. Append-then-acknowledge means every acknowledged event is guaranteed to exist in the log that score aggregation, projection, and disaster recovery (Component 18) all read from.

The dedup-store check

pseudocodeAtomic check-and-set dedup gate — the primitive every update function in Components 05–07 calls before touching any score state
function checkAndMarkEvent(eventId, dedupStore):
    // Must be a single atomic check-and-set, not a separate read then write --
    // two concurrent deliveries of the same eventId must not both see "not seen yet"
    wasAlreadySeen = dedupStore.setIfAbsent(eventId, markedAt = now(), ttl = DEDUP_RETENTION_WINDOW)
    return wasAlreadySeen ? DedupResult.DUPLICATE : DedupResult.FIRST_SEEN

The retention window matters: a dedup store that forgets an event ID too soon reopens the door to double-processing a very late redelivery; one that remembers forever is a real, unbounded storage cost. The window should be sized against the slowest realistic redelivery delay the ingestion pipeline is expected to tolerate (retry backoff ceilings, consumer restart recovery time), with events older than that window relying on the authoritative row's own last_event_id and monotonic/unique-delta guards as a second line of defense rather than the dedup store alone.

Consumer retry and dead-lettering

A consumer that fails mid-processing (crashes after reading an event but before committing its effect) must be safe to simply restart and reprocess from its last committed offset — which is exactly why the dedup check runs first, inside the same logical unit of work as the score update, rather than as a best-effort filter applied earlier in the pipeline. An event that fails processing repeatedly (malformed payload, a referenced player ID that does not exist, a leaderboard identity that is not open) is routed to a dead-letter queue after a bounded number of attempts rather than retried indefinitely — an indefinitely-retried poison message would otherwise block every event behind it in the same ordered partition.

Key takeaways
  • Score events are only ever accepted from authenticated game-server sessions — never trusted directly from a player's client
  • Durable append happens before acknowledgment, so an acknowledged event is guaranteed to exist in the log consumers read from
  • The dedup check is an atomic check-and-set against the event ID, called inside the same unit of work as the score update, not as a separate best-effort filter
  • A bounded retry count with dead-lettering keeps one poison message from blocking every event behind it in the same ordered partition
Component 09

Ordering and Concurrent Updates

Two conflicting events for the same player can be processed by two different consumer workers at nearly the same instant — the version counter from Component 04 is what turns that race into a deterministic outcome instead of a lost update.

Player-scoped sequence numbers

Each authoritative row's version field is a player-and-leaderboard-scoped sequence number: every accepted write increments it by exactly one, and every conditional update names the version it expects to find. This is deliberately scoped per row, not global — two different players' updates never contend with each other, which is what lets the ingestion pipeline process unrelated players' events fully in parallel while still serializing conflicting updates to the *same* player's row.

Compare-and-set under concurrent conflicting events

pseudocodeCompare-and-set prevents a lost update: Worker 2's 9,600 is still better than the now-current 9,400 and must be retried against the fresh version, not discarded
// Two workers race to apply different events for the same player at roughly
// the same time. Both read version = 7. Only one write can win against that version.

// Worker 1: candidate value 9,400
rowsAffected = conditionalUpdate(playerId, leaderboardId, expectedVersion = 7, newValue = 9400, ...)
// rowsAffected == 1 -- Worker 1 wins; row is now version 8

// Worker 2: candidate value 9,600 (a genuinely better score, arrived almost simultaneously)
rowsAffected = conditionalUpdate(playerId, leaderboardId, expectedVersion = 7, newValue = 9600, ...)
// rowsAffected == 0 -- version is no longer 7; Worker 2 MUST re-read and retry,
// never assume its own write silently failed to apply or that it can be skipped

The critical detail is that Worker 2's conflict is not automatically "my update didn't matter" — it has to re-read the row (now at value 9,400, version 8) and reapply its own comparison (9,600 > 9,400 → still an improvement) before retrying the conditional update. Treating any rowsAffected == 0 outcome as "someone else already won, I can stop" would silently drop a legitimate update whenever the two concurrent events are not mutually exclusive in effect — exactly Worker 2's situation here.

Event-time vs. processing-time

achieved_at (event-time: when the player actually earned the result, as reported by the authenticated game server) and updated_at / dedup-store timestamps (processing-time: when the ingestion pipeline happened to handle the event) are different clocks measuring different things, and only one of them is allowed to influence ranking. Test D's late 9,200 submission is a processing-time-late event — it was handled well after evt-101 — but it was rejected purely on value comparison, not on arrival order. Event-time (achieved_at) only matters as the tie-break field (Component 03) when two values are genuinely equal; it is never used to decide whether an update is "too old to apply" on a best-score or cumulative board, because validity there is defined entirely by magnitude and uniqueness, not recency.

Key takeaways
  • Version numbers are scoped per player-and-leaderboard row, letting unrelated players' updates process fully in parallel
  • A version conflict means re-read and reapply the comparison against the fresh row — not "someone else already won, stop" — or legitimate concurrent improvements get silently dropped
  • achieved_at (event-time) and updated_at/dedup timestamps (processing-time) measure different things; only achieved_at is ever allowed to influence rank, and only as a tie-break
  • A delayed event is handled by comparing its value against the current stored value, never by comparing its arrival time against other events' arrival times
Component 10

Materialized Rank Index and the Dual-Write Problem

The authoritative row (Component 04) is the system of record; the sorted set is a materialized projection of it, kept queryable for rank, top-N, and nearby-rank reads without ever hitting the row store for every leaderboard view. How that projection gets built is the one place a naive implementation quietly reintroduces inconsistency.

The dual-write problem, stated honestly

The naive approach — write the authoritative row, then separately write the sorted set, as two independent operations against two independent stores — has an unavoidable failure window: if the process crashes, the network drops, or the second store is briefly unavailable between the two writes, the authoritative row and the projection disagree, and nothing in that design detects or repairs the disagreement on its own. This is not a rare edge case to shrug off; at sustained event volume, some nonzero rate of exactly this interleaving is a near-certainty over time, and a design that depends on it never happening is depending on something false.

Game ServerGatewayauth + validateEvent Logdurable, orderedProcessordedup + aggregateScores DBauthoritativeRedis ZSETmaterializedaggregateprojectDurable commit happens at the event log — DB and Redis are downstream projections
Durable commit happens once, at the event log — the authoritative store and the Redis sorted set are both downstream, independently-rebuildable projections of that one log

Event-driven projection as the fix

The fix is to make the durable, ordered event log (Component 08) the single commit point, and treat both the authoritative row store and the sorted set as separate, independent *consumers* of that same log — each applying the same idempotent update logic from Components 05–07 against its own storage, each able to replay from the log and reconstruct its own state from scratch if it ever falls behind or gets corrupted (Component 18). Neither consumer's state is derived from the other's; both are derived from the one thing that was actually durably committed before acknowledgment. A write to the authoritative row succeeding while the sorted-set consumer is temporarily behind is not an inconsistency to detect and repair after the fact — it is an expected, bounded, self-healing lag, not a dual-write failure.

Projection lag is real and must be bounded, not ignored

"Real time" never means zero latency; it means an explicit, monitored staleness budget. There are three genuinely distinct moments in this pipeline worth naming separately: write-ack (the event is durably appended to the log and the game server's submission is acknowledged), materialized-rank visibility (the sorted-set projection has caught up and a rank query now reflects this event), and client notification (a push update actually reaches a connected client — which can be delayed or missed independently of the first two, see Component 15). A design that conflates these into one "it's real-time" claim cannot explain what "ingest-to-rank lag" in its own monitoring (Component 20) is even measuring.

Key takeaways
  • Writing the authoritative row and the sorted set as two independent, separately-failable operations is the dual-write problem — not a rare theoretical risk, a near-certain eventual inconsistency at scale
  • The fix: one durable commit point (the event log), with both the row store and the sorted set as independent, idempotent, replayable consumers of it
  • Projection lag between the authoritative write and materialized-rank visibility is expected and must be bounded and monitored, not treated as an inconsistency to patch over
  • Write-ack, materialized-rank visibility, and client notification are three distinct moments — never collapse them into a single undifferentiated "real-time" claim
Component 11

Query Algorithms: Top-N, Own Rank, Nearby Rank

Every leaderboard read — top-N, own rank, the window of players around a given player — is a query against the materialized sorted set, and every one of them is only meaningful relative to a specific, named board version (Component 15), not an implicit "whatever is current right now."

Player ClientLeaderboard APIRedis ZSETtop-N / rankRead Replicabounded-staleFriends Serviceeligible setZREVRANGE / ZREVRANKtop-N fan-outfriend set lookup
Top-N, own-rank, and friends-set queries all read from the materialized projection, not the authoritative row store
textTop-N and own-rank reads against the Arena Clash board
ZREVRANGE leaderboard:arena-clash:solo:S7:global 0 2 WITHSCORES
# top 3: p-ava 9800, p-ben 9500, p-chloe 9500  (tie order already resolved by the tie-aware index, Component 03)

ZREVRANK leaderboard:arena-clash:solo:S7:global p-dev
# own rank: 0-indexed position -> displayed as (index + 1)

Nearby-rank (rank-around-player) window

pseudocodeNearby-rank window: clamp both edges, inclusive range, operating against an index that already encodes the correct tie-break order
function nearbyRank(playerId, leaderboardId, window):       // window = 1 means +/-1 inclusive
    centerRank = rankAwareIndex.getRank(playerId, leaderboardId)   // 0-indexed, tie-break already resolved
    if centerRank == null:
        return Result.PLAYER_NOT_RANKED

    boardSize = rankAwareIndex.size(leaderboardId)
    loRank = max(0, centerRank - window)
    hiRank = min(boardSize - 1, centerRank + window)             // clamp at both edges

    return rankAwareIndex.getRange(leaderboardId, loRank, hiRank, withScores = true)   // inclusive

Test F, traced

From the original state, the correct (tie-aware) ranking is Ava(0), Ben(1), Chloe(2), Dev(3), Eli(4) in 0-indexed rank. Chloe's centerRank is 2. With window = 1: loRank = max(0, 1) = 1, hiRank = min(4, 3) = 3. The inclusive range [1, 3] returns Ben (rank 1 → #2), Chloe (rank 2 → #3), Dev (rank 3 → #4) — exactly the required window, and exactly why the clamping matters: had Chloe been at rank 0 or the last rank, the window would correctly shrink rather than wrap or error.

Stable pagination via snapshot version

A paginated top-N view ("page 2 of the leaderboard") is only internally consistent if every page request within one browsing session is answered against the same board version — otherwise a rank change between page 1 and page 2 requests can duplicate or skip a player across the page boundary. The pagination contract is: the first page response carries the board version it was computed against; subsequent page requests pass that version back; and the query layer either serves subsequent pages from the same materialized version (if still retained) or signals the client that the view is stale and a fresh page 1 is needed (Component 15 covers exactly this contract for live updates, not just pagination).

Key takeaways
  • Top-N and own-rank are direct queries against the materialized, tie-aware projection — not against the authoritative row store
  • Nearby-rank windows must clamp at both the top and bottom of the board and read an inclusive range against a tie-break-resolved index
  • Test F: Chloe at rank 2 (0-indexed), window ±1 → ranks [1,3] → Ben(#2), Chloe(#3), Dev(#4)
  • Pagination is only stable when every page of one browsing session is answered against the same named board version
Component 12

Friends-Only Leaderboard

A friends-only view is never a filter applied to a precomputed global top-N — by construction, a player's friends are almost never clustered at the top of the global board, so the only correct approach is to resolve the eligible set first and rank that small set directly.

Eligible-set resolution and the privacy boundary

The leaderboard service asks an external friends service one question: "which of these player IDs are in this player's eligible set?" — it never asks for, stores, or needs the requesting player's full social graph. This is a deliberate privacy boundary: the friends service is the sole owner of relationship data, and it exposes eligibility (a yes/no per candidate ID, or a bounded list of friend IDs for a known player) as its only interface to the leaderboard, never a dump of the underlying social graph itself.

pseudocodeFriends leaderboard: resolve eligibility externally, batch-fetch scores for that small set, sort with the same comparator
function getFriendsLeaderboard(playerId, leaderboardId):
    friendIds = friendsService.getFriendIds(playerId)          // external interface -- eligibility only
    if friendIds.isEmpty():
        return []

    rows = scoreStore.batchGetRows(friendIds, leaderboardId)   // small, bounded batch -- not a board-wide scan
    rows = rows.filter(r -> r.status == ACTIVE)                 // respect moderation state (Component 17)

    rows.sortBy(compareForRank, policy)                         // same comparator as the global board
    return attachRanks(rows)                                    // ranks are LOCAL to this subset, not global rank
textBatch score lookup for the eligible subset — a single round trip for all of a player's friends, not one call per friend
ZMSCORE leaderboard:arena-clash:solo:S7:global p-ben p-dev p-eli
# 1) "9500"
# 2) "9100"
# 3) "8700"

Test E, traced

Ava's friends are Ben, Dev, and Eli. From the original state, their scores are Ben 9,500, Dev 9,100, Eli 8,700 — Ava herself and Chloe are not in this eligible set at all. Sorting just these three by the global comparator (no ties in this subset, so the tie-break never even has to run) gives Ben #1, Dev #2, Eli #3 — ranks that exist only within this friends view. This is not derivable by taking the global top-N and filtering down to friends: Ben's *global* rank is #2, not #1, and Dev's global rank is #4, not #2. Any implementation that tries to shortcut friends-ranking by filtering a cached global top-N will silently produce wrong ranks (or miss friends entirely) whenever a friend falls outside whatever N was cached.

Caching the friends view

Because an eligible set is typically small and stable (friend lists change far less often than scores), caching the resolved friend-ID list per player for a short TTL is a reasonable optimization — but the score lookup and sort for that set should stay close to real-time, since this view's entire value proposition is showing a player how they compare to people they actually know right now.

Key takeaways
  • Friends-only rank is computed by resolving the eligible set, batch-fetching only those scores, and sorting that small subset with the same comparator — never by filtering a cached global top-N
  • The friends service's interface is eligibility only (friend IDs, or yes/no per candidate) — it never exposes the full social graph to the leaderboard service
  • Test E: Ben #1 (9,500), Dev #2 (9,100), Eli #3 (8,700) — none of these equal their global rank
  • Friend-ID list resolution can be cached briefly; the score lookup and sort for that set should stay close to real-time
Component 13

Sharding and Exact Global Ranks

Sharding by board identity (one sorted set per game+mode+season+scope) is cheap and keeps every rank query exact. Sharding a single, enormous board across multiple partitions is a fundamentally harder problem: a shard-local rank is not a global rank, and pretending otherwise produces a leaderboard that is confidently wrong.

Two different kinds of partitioning

Partitioning at the key level — arena-clash's board lives on one shard, a different game's board lives on another — changes nothing about correctness: each board is still a single, complete sorted set, and ZREVRANK against it is still exact. The harder case is partitioning *one* board's player population across multiple shards because it has grown too large or too hot for one node (Component 14) — now no single shard holds the complete ordering, and ZREVRANK against any one shard only answers "what is this player's rank among the players on this shard," which is a different, smaller question than "what is this player's rank among everyone."

Shard 1local rank #4Shard 2local rank #2Shard 3local rank #7Merge / Approximateglobal rank ≈ #11, not exact
Three shards each report a correct local rank; merging them into one global rank requires more than concatenating the numbers

Merge-top-N is cheap; exact global rank-counting is not

Merging *top-N* across shards is the easy half of this problem: ask each shard for its own top-N, merge the small resulting lists with the shared comparator, and take the top-N of the merge — cheap, because only a small, bounded number of rows per shard are ever involved. Answering "exactly how many players outrank this specific player, globally" is the hard half: it requires knowing, for every shard, how many of *that shard's* players have a value better than this player's value — which means either a cross-shard count query against every shard for every such request, or a separate, continuously-maintained global counting structure that every shard's writes must keep updated. Neither of those is free, and both scale with the number of shards in a way merge-top-N does not.

Exact vs. approximate, as an explicit operational trade-off

A system can choose to pay the real cost of exact cross-shard rank-counting on every own-rank request (correct, but a query that fans out to every shard and does real counting work), or it can choose an approximate global rank — periodically recomputed, or estimated from each shard's local rank plus known shard-size statistics — accepting that a player's displayed "global rank" may lag or be off by a small, bounded amount between recomputations. Neither choice is universally correct: the right one depends on how often own-rank is actually queried, how large N has grown, and how much rank precision the product genuinely needs versus how much it merely assumes it needs. What is not acceptable is silently presenting a shard-local rank as if it were the exact global rank — that is not an approximation, it is simply the wrong number.

Key takeaways
  • Partitioning by board identity is free correctness-wise — each board stays a single, complete, exactly-rankable sorted set
  • Partitioning one oversized board across shards means no single shard holds the full ordering — a shard-local ZREVRANK is a different, smaller question than the global rank
  • Merge-top-N across shards is cheap (bounded rows per shard); exact global rank-counting is expensive (scales with shard count, not just result size)
  • Choosing approximate global rank is a legitimate, explicit trade-off; silently presenting a shard-local rank as the exact global rank is simply incorrect, not an approximation
Component 14

Hot-Key Mitigation

A single wildly popular board's sorted set lives behind effectively one key, and most single-key data-plane operations in a store like Redis are served by one thread for that key — so a board experiencing a sustained write and read burst can become a real, singular bottleneck well before the rest of the cluster is under any pressure at all.

ONE HOT KEYAll Boardssingle ZSET — CPU 95%PARTITIONED BY BOARD IDENTITYBoard Aisolated capacityBoard BunaffectedBoard Cunaffected
One board as a single hot key vs. a board partitioned across multiple keys with isolated capacity per partition

Read replicas and top-N caches

Top-N reads and own-rank reads are, by a wide margin, more frequent than writes on a popular board — so routing reads to replicas (accepting a small, bounded, explicit staleness window) takes read pressure off the single writer entirely. A short-TTL cache in front of the most commonly requested view (top-10, top-100) further reduces load, as long as the cache's TTL is understood as a deliberate staleness bound rather than an accidental one — consumers of that cached view should know, approximately, how stale "top-10 right now" might be.

Update batching and coalescing

If a burst of events for the same handful of players arrives in a tight window (a popular tournament moment, a game mode ending simultaneously for thousands of matches), coalescing multiple pending updates for the same player into a single write — applying only the final, best resulting value rather than one write per event — reduces write volume against the hot key without changing the final outcome, as long as the coalescing logic still respects the same monotonic/unique-event rules as the uncoalesced path. This is strictly an optimization on write volume, not a change to correctness semantics.

Bounded queues and fairness

The ingestion queue feeding a hot board's projection should be explicitly bounded, with backpressure applied upstream (slowing or rejecting further intake) rather than allowed to grow without limit — an unbounded queue in front of an overloaded single key just delays the overload's visibility while making recovery slower and lag spikier when it finally catches up. Partition choice matters for fairness too: if a board's players are ever split across multiple partitions (Component 13), the partitioning scheme should not concentrate a disproportionate share of the most active players onto one partition, which would simply relocate the same hot-key problem rather than solve it.

Key takeaways
  • A single very popular board's sorted set is effectively one key, and most stores serve one key's writes through one thread — a real, specific bottleneck, not a generic capacity problem
  • Read replicas with an explicit staleness bound, and short-TTL top-N caches, take the bulk of read pressure off the single writer
  • Coalescing multiple pending updates for the same player into one write reduces hot-key write volume without changing correctness, as long as the same monotonic/unique-event rules still apply
  • Ingestion queues in front of a hot board must be explicitly bounded with upstream backpressure — an unbounded queue only delays and worsens the eventual overload
Component 15

Live Update Protocol: Snapshots, Deltas and Versions

A connected client's view of the leaderboard is kept current by a versioned snapshot-plus-delta protocol — every update the client applies is tied to an explicit board version, and a client that cannot prove it is caught up never silently keeps displaying what it already has as if it were current.

TransportDelivery modelTypical fit
PollingClient periodically re-requests the current viewSimplest to operate; freshness is bounded by poll interval, not push latency
Server-Sent Events (SSE)Server pushes a one-way stream of versioned deltasGood fit when updates flow server→client only and the client never needs to push state back over the same channel
WebSocketBidirectional persistent connectionNeeded when the same channel also carries client→server messages (e.g. live chat alongside rank updates); otherwise SSE's simplicity is often enough
Transport options for live leaderboard updates
Processorcommits v46Live Update Svcversioned deltasClient Alast-applied v46Client Blast-applied v46Client Clast-applied v42 → gap!delta v46delta v46resync 43–46
The processor commits board version 46; clients already at 46 get nothing new, a client at 42 needs a contiguous replay of versions 43 through 46

Test I, traced

A client's last-applied board version is 42; the server's current committed version is 46. The live-update service checks whether versions 43 through 46 are still retained in its recent-delta buffer. If they are, it replays exactly that contiguous range to the client, in order, and the client applies each delta in sequence until it reaches 46 — at which point its view is provably caught up, not merely assumed to be. If version 43 (or any version in the needed range) has already fallen out of the retention window, a contiguous replay is no longer possible, and the only correct response is a full snapshot at the current version — never a partial replay that silently skips the missing range, which would leave the client's state permanently and invisibly wrong relative to what it thinks it has.

pseudocodeClient reconciliation: contiguous delta replay when possible, full snapshot resync when the needed range has already aged out
function reconcileClient(clientVersion, server):
    if clientVersion == server.currentVersion:
        return NoOp

    if server.deltaBufferHasContiguousRange(clientVersion + 1, server.currentVersion):
        return ReplayDeltas(server.getDeltas(clientVersion + 1, server.currentVersion))   // Test I's 43-46 case
    else:
        return FullSnapshot(server.currentVersion)   // retention expired -- never a partial/gapped replay

Coalescing and out-of-order protection

When several board versions commit in a very short window, the live-update service may coalesce them into a single delta describing the net effect from the client's last version to the newest — this is a pure optimization on how much is sent over the wire, not a change to which version the client ends up at. On the client side, a delta that does not immediately extend the client's current version (an out-of-order or duplicate delivery) is never applied blindly; the client checks the delta's base version against its own last-applied version and requests reconciliation (the function above) whenever they do not match exactly.

Subscription lifecycle

A client subscribes to a specific leaderboard identity (not "the leaderboard" generically) and receives an initial full snapshot plus its version as the subscription's starting point. On reconnect after a dropped connection, the client always presents its last-known version and goes through the same reconciliation path above — a reconnect is never treated as equivalent to a fresh subscription that silently resets state, since the client may have missed deltas during the disconnect window that it still needs reconciled.

Key takeaways
  • SSE fits one-way server→client push well; WebSocket earns its bidirectional cost only when the same channel also needs client→server traffic; polling trades push latency for operational simplicity
  • Test I: client at v42, server at v46 → replay the contiguous range 43-46 if still retained, or send a full snapshot if that range has already aged out — never a partial, gapped replay
  • Coalescing multiple committed versions into one delta changes only wire efficiency, never which version the client ends up at
  • A reconnect is handled by the same version-reconciliation path as any other gap — never treated as a fresh subscription that silently drops missed state
Component 16

Season State Machine

A season is not just a label on a leaderboard row — it is a state machine, and the transition boundaries (especially the moment writes stop) are exactly where a careless implementation lets a late event corrupt a finalized result or silently leak into the wrong season.

Openaccepting scoresFreezeno new writesFinalizeimmutable resultArchiveread-only historyA late event after Freeze is held for an explicit, audited correction — never silently applied
Open → Freeze → Finalize → Archive, with a controlled correction path for any event that arrives after Freeze
StateAccepts new score events?Reachable only by
OpenYes — the normal ingestion path from Components 05-08Season creation, or the prior season's Archive transition
FreezeNo — events are held/quarantined, not appliedAn explicit, logged freeze command at a specific event-log offset
FinalizeNo — results are now immutable to ordinary writesFreeze, once any in-flight events at or before the freeze offset have drained
ArchiveNo — read-only historical recordFinalize, once the next season has been independently opened
Season states and what each one allows

The race at the freeze boundary

"Freeze at a wall-clock time" is an ambiguous instruction the moment events are still in flight through an ingestion pipeline with any processing delay: an event genuinely submitted before the cutover instant can still be sitting in a queue when the freeze command executes. The fix is the same idea Component 09 already relies on — anchor the cutover to a position in the durable, ordered event log, not to a clock reading. Freezing records "season S7 closes as of log offset X"; every event at or before offset X is still eligible to apply normally, and every event after it is held for the correction path below, regardless of which wall-clock instant either event happened to be processed at.

pseudocodeSeason write-path check: the event's own explicit season tag determines its board, independent of whatever season happens to be "current" right now
function applySeasonScopedEvent(event):
    board = resolveBoard(event.game, event.mode, event.season, event.scope)   // explicit season tag on the event itself

    if board == null:
        return Outcome.REJECTED_UNKNOWN_SEASON

    if board.state == FROZEN or board.state == FINALIZED or board.state == ARCHIVED:
        quarantineForCorrection(event, board)        // never silently applied, and never re-routed to a different season
        return Outcome.HELD_FOR_CORRECTION

    return applyScoreEvent(event, board)             // the normal Component 05/06/07 path -- board.state == OPEN

Test H, traced

Season S7 is finalized; season S8 opens as a brand-new, empty board (arena-clash / solo / S8 / global) with its own independent event stream and its own fresh row store — nothing carries over implicitly. A late event that was genuinely generated during S7 still carries an explicit season: S7 tag (set at the moment the game server recorded it, not inferred later from "whatever season is open when this finally gets processed"). When that event reaches applySeasonScopedEvent, resolveBoard finds the S7 board, sees its state is FINALIZED, and routes the event to quarantine — it can never mutate S7's now-immutable result, and because its season tag is explicitly S7, it is structurally impossible for it to be misapplied to S8 by accident either.

Controlled correction, not silent reopening

A quarantined post-freeze event is not automatically discarded, and it is not automatically applied either — it waits for an explicit, audited decision: a human or automated moderation review determines whether it represents a legitimate result that genuinely occurred before the freeze boundary (and should be applied via a logged correction, re-ranking S7's archived result) or a late/invalid submission that should be formally rejected. Either outcome is itself an audited event in the log, never a direct edit to the archived rows — the same principle Components 06 and 17 apply to any other correction.

Key takeaways
  • Freeze is anchored to an event-log offset, not a wall-clock instant, because in-flight events make "freeze at time T" ambiguous on its own
  • Every event carries its own explicit season tag — a finalized season's late event can never be silently reinterpreted as belonging to the newly-opened season
  • Test H: a late S7 event reaches the S7 board (now FINALIZED), is quarantined, and can neither mutate S7 nor leak into the empty S8 board
  • A quarantined post-freeze event is resolved only through an explicit, audited correction decision — never silently discarded or silently applied
Component 17

Anti-Cheat and Moderation

Server authority (Component 08) keeps an untrusted client from inventing a score outright; moderation handles the harder case — a score that passed ingestion as a syntactically valid, server-attributed event, but later turns out to be implausible or fraudulent, and now has to be removed from a board that other players have already seen and reacted to.

Plausibility checks and suspicious-event quarantine

Before (or shortly after) a score applies, automated plausibility checks can flag an event as suspicious without necessarily blocking it outright — a score far outside the statistical range of legitimate play for that game mode, an implausibly short time-to-achieve relative to the minimum physically possible play time, or a pattern correlated with known exploit signatures. A flagged event can route to a holding/quarantine state pending review rather than applying immediately when the risk is high enough, or apply immediately with a pending_review status when the product favors responsiveness over caution for lower-risk flags — which policy to use for which signal is a product and trust-and-safety decision, not a universal rule.

Invalidation as an audited correction event

pseudocodeInvalidation is appended as its own audited event and replayed against event history — never a silent in-place edit of the row or the projection
function invalidateScore(playerId, leaderboardId, originalEventId, reason, moderator):
    correctionEvent = {
        eventId: generateUniqueId(),             // the correction is itself a new, unique, dedup-able event
        type: SCORE_INVALIDATION,
        targetEventId: originalEventId,           // audit trail back to exactly what is being invalidated
        playerId, leaderboardId, reason, moderator,
        issuedAt: now()
    }
    appendToEventLog(correctionEvent)             // never a direct UPDATE/DELETE against the row or the sorted set

    bestRemainingValue = recomputeFromEventHistory(playerId, leaderboardId, excluding = originalEventId)
    if bestRemainingValue == null:
        setRowStatus(playerId, leaderboardId, status = INVALIDATED)   // no valid score remains -- player becomes unranked
    else:
        applyWithOptimisticRetry(playerId, leaderboardId, candidateValue = bestRemainingValue, ...)

    triggerReRank(leaderboardId)                  // neighbors shift as a consequence, not a separately-issued correction

Test G, traced

From the original state, Ava's 9,800 is invalidated, and no earlier valid score exists in her event history for this board — recomputeFromEventHistory finds nothing to fall back to. Her row's status becomes INVALIDATED, and she is excluded from ranking entirely (unranked, not ranked-last). Re-ranking the remaining active rows produces Ben #1 (9,500), Chloe #2 (9,500, still behind Ben by the same achieved-at tie-break from Component 03), Dev #3 (9,100), Eli #4 (8,700) — every neighbor shifts up by exactly one position as a direct, mechanical consequence of the comparator being re-evaluated over the smaller active set, not because each of them was individually re-ranked by a separate correction.

Key takeaways
  • Suspicious events can be quarantined pending review or applied with a pending_review status, depending on signal confidence and product tolerance for latency versus risk
  • Invalidation is appended as its own audited correction event with a reason and an attributable moderator/system actor — never a silent in-place edit
  • Test G: Ava's only valid score is invalidated with no fallback, so she becomes unranked; Ben, Chloe, Dev, and Eli each shift up one position as a mechanical consequence of re-evaluating the comparator over the smaller active set
  • Re-ranking neighbors after a correction is a side effect of the comparator re-running, not a separate correction issued per affected player
Component 18

Disaster Recovery and Rebuild

Because every algorithm in this page is idempotent and driven from one durable, ordered event log, disaster recovery is not a special-case procedure bolted on afterward — it is the same ingestion and update logic, run from the beginning of the log instead of from the current tail.

Last SnapshotReplay Event Logdeterministic orderRebuilt ZSETidentical to pre-crash
Rebuild starts from the last trusted snapshot, replays the event log deterministically, and produces a sorted-set index identical to the one lost in the incident

Replay reconstructs aggregates, then the rank index

pseudocodeRebuild is replay: the same idempotent update functions, run over the full (or snapshot-resumed) event history in committed order
function rebuildLeaderboard(leaderboardId, eventLog, lastTrustedSnapshot):
    scoreStore = lastTrustedSnapshot != null ? loadSnapshot(lastTrustedSnapshot) : emptyStore()
    startOffset = lastTrustedSnapshot != null ? lastTrustedSnapshot.offset : 0

    for event in eventLog.readFrom(startOffset):              // strict committed order -- never re-sorted or batched out of order
        if event.leaderboardId != leaderboardId:
            continue
        applyScoreEvent(event, scoreStore)                      // the exact same Component 05/06/07 logic, dedup included

    rankIndex = projectToSortedSet(scoreStore)                  // authoritative aggregate -> fresh sorted-set projection
    return rankIndex

Checksums, comparison, and a safe rollout

A rebuild is validated before it is trusted: the rebuilt aggregate store's checksum (a hash over every row's player ID, value, achieved-at, and version) is compared against the last known-good checksum taken at a matching log offset, and any divergence is investigated before the rebuilt index is promoted — not after. The rebuild itself runs into a shadow or staging index rather than overwriting the live projection in place; only once the rebuilt shadow index is checksum-verified (or, for a full from-scratch rebuild with no prior snapshot to compare against, spot-checked against a sample of known-correct historical ranks) does a deliberate cutover swap read traffic from the old projection to the new one. This blue/green-style swap means a rebuild that turns out to be wrong never becomes visible to players before anyone notices.

Determinism is what makes any of this trustworthy: replaying the identical committed event sequence, including every correction and invalidation event from Component 17, must reconstruct bit-for-bit the same final state every time it is run — no dependence on wall-clock reads during replay, no dependence on hash-iteration order, no dependence on anything other than the event log's own already-fixed committed order.

Key takeaways
  • Disaster recovery is the same idempotent update logic from Components 05-08, run from the last trusted snapshot (or the start) through the event log's committed order
  • A rebuild's checksum is compared against a known-good checksum at a matching offset before the rebuild is trusted, not after it is already serving traffic
  • Rebuilds land in a shadow/staging index first; a deliberate, verified cutover swaps live read traffic over, rather than overwriting the live projection in place
  • Deterministic replay — including every correction and invalidation event — must reproduce bit-for-bit identical state every time, which is the property the rest of this page's "never silently edit in place" rules exist to protect
Component 19

Multi-Region Architecture

Serving players across continents without a single, far-away round trip for every score submission means picking an explicit write-authority model per board — and being honest that every option trades away something real, not finding a way around the trade-off.

Game ServersGatewayEvent LogProcessorScores DBPartitioned ZSETsRead ReplicasLive Update SvcSubscribersModerationSeason Lifecycleprojectaudit
The complete pipeline from Components 08-18, deployed with one authoritative region per board and region-local read paths elsewhere

One authoritative region per board

Rather than attempting multi-region active-active writes to the same leaderboard identity (which reintroduces exactly the kind of concurrent-conflicting-update problem Component 09 already has to solve, now with cross-region network latency added to every conflict resolution), each board identity is assigned a single authoritative region — commonly the region nearest to where that board's match/game-server traffic is predominantly generated. All score events for that board, regardless of which region a given player's game session happens to connect through, are forwarded to and committed in that one authoritative region's event log.

Region-local read caches and replication lag

Other regions serve reads (top-N, own-rank, friends view) from a region-local replica or cache that is kept current via cross-region replication of the authoritative event log or the materialized projection itself. That replication has real, non-zero lag — illustratively on the order of tens to a few hundred milliseconds depending on inter-region network distance, an assumption for discussion here, not a measured figure for any specific deployment — and a player reading from a non-authoritative region is, by construction, looking at a slightly stale view relative to the authoritative region's current state. This is the same write-ack-vs-materialized-visibility distinction from Component 10, with an additional, larger, and more variable hop added for cross-region replication specifically.

Cross-region failure handling, honestly

If the authoritative region for a board becomes unreachable, there are two genuinely different responses, and neither is free: serve reads from the last-replicated state in a surviving region while refusing new writes until the authoritative region recovers (simple, but a real write-availability gap for that board's players during the outage), or promote a different region to authoritative status through an explicit, coordinated failover that fences the old region's writer from being able to resume writing once it comes back (preventing a split-brain where two regions both believe they are authoritative), at the cost of real failover-coordination latency and operational complexity. Claiming either option as "seamless" or "zero-downtime" misrepresents what is actually happening during the failure window — this is a genuine availability-versus-consistency trade-off playing out at the region level, not a problem multi-region deployment makes disappear.

Key takeaways
  • Each board identity has one authoritative region for writes — active-active writes to the same board across regions would reopen the conflicting-update problem with cross-region latency added
  • Non-authoritative regions serve reads from a replica kept current via cross-region replication, which carries real, illustrative-only lag figures — never a zero-latency claim
  • A region failure forces an explicit choice: pause writes for that board until recovery, or fail over to a new authoritative region with coordinated fencing against the old one
  • Multi-region deployment is a genuine trade-off between availability and consistency during a failure window — not a trade-off that goes away because more regions were added
Component 20

Security and Observability

Every correctness guarantee in this page depends on two things holding operationally: that only authenticated, authorized game-server identities can ever submit a score event, and that the gap between a committed event and a materialized, visible rank is continuously measured rather than assumed to be small.

Rate limits and access control

Score-submission credentials are issued to authenticated game-server processes only, never to player clients directly — the access-control boundary from Component 08 enforced again here as an explicit security posture, not just a data-correctness mechanism. Per-credential rate limits on event submission protect the ingestion pipeline from a single compromised or misbehaving server instance flooding a board, and separate, much more permissive rate limits apply to read traffic (top-N, own-rank, friends view), since legitimate read volume from a popular game can be orders of magnitude higher than legitimate write volume.

Personal-data exposure

The friends-only view (Component 12) is the one place this system handles social-relationship data, and the privacy boundary established there is also a security boundary: the leaderboard service receives eligibility answers from the friends service, never a raw social graph, and a friends-view response to one player must never include another player's friend list, only that other player's score and rank within the requesting player's own eligible set. A bug that leaked a full friend list through a leaderboard response would be a privacy incident independent of anything else in this design being correct.

Core observability signals

SignalWhat it measuresWhy it matters
Ingest-to-rank lagTime from durable event-log commit (write-ack) to that event's effect being visible in the materialized rank indexDirectly measures the projection lag Component 10 says must be bounded and monitored, not assumed
Rank-consistency checksPeriodic comparison of the materialized index's order against the comparator re-evaluated from the authoritative rowsCatches tie-break or comparator-direction bugs (Components 03, 07) before players notice an impossible ordering
p99 query latencyTail latency for top-N, own-rank, and nearby-rank readsTail, not average, latency is what a player actually experiences as "the leaderboard feels slow"
Ingestion backlog depthQueued-but-unprocessed event count feeding the projectionEarly warning of hot-key pressure (Component 14) or a stalled consumer, before lag becomes player-visible
Key operational signals this architecture's own guarantees depend on
Key takeaways
  • Score-submission credentials belong to authenticated game servers only; read and write traffic get separately-tuned rate limits reflecting their very different legitimate volumes
  • The friends-service boundary is a security boundary as much as a design one: eligibility and scores cross it, full social graphs never do
  • Ingest-to-rank lag, rank-consistency checks, p99 query latency, and ingestion backlog depth are the signals that make this architecture's own correctness and performance guarantees observable rather than assumed
  • Every lag and latency figure in this page is explicitly illustrative — never a verified benchmark for any real deployment