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.
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.
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."
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)- 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
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.
Complexity, conceptually — not a measured benchmark
| Operation | Conceptual complexity | Why |
|---|---|---|
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 |
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.
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"- 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
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.
The concrete tie comparator
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 winsTest 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.
- 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
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.
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_BESTTest 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.)
- 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
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.
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.APPLIEDZINCRBY 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 appliedTest 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).
- 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
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.
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_TIMEfunction 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_FIRSTZADD 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).
- 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
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.
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
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_SEENThe 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.
- 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
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
// 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 skippedThe 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.
- 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
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.
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.
- 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
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."
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
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) // inclusiveTest 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).
- 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
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.
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 rankZMSCORE 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.
- 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
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.
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.
- 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
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.
| Transport | Delivery model | Typical fit |
|---|---|---|
| Polling | Client periodically re-requests the current view | Simplest to operate; freshness is bounded by poll interval, not push latency |
| Server-Sent Events (SSE) | Server pushes a one-way stream of versioned deltas | Good fit when updates flow server→client only and the client never needs to push state back over the same channel |
| WebSocket | Bidirectional persistent connection | Needed when the same channel also carries client→server messages (e.g. live chat alongside rank updates); otherwise SSE's simplicity is often enough |
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.
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 replayCoalescing 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.
- 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
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.
| State | Accepts new score events? | Reachable only by |
|---|---|---|
| Open | Yes — the normal ingestion path from Components 05-08 | Season creation, or the prior season's Archive transition |
| Freeze | No — events are held/quarantined, not applied | An explicit, logged freeze command at a specific event-log offset |
| Finalize | No — results are now immutable to ordinary writes | Freeze, once any in-flight events at or before the freeze offset have drained |
| Archive | No — read-only historical record | Finalize, once the next season has been independently opened |
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.
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 == OPENTest 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.
- 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
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
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 correctionTest 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.
- 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
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.
Replay reconstructs aggregates, then the rank index
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 rankIndexChecksums, 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.
- 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
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.
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.
- 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
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
| Signal | What it measures | Why it matters |
|---|---|---|
| Ingest-to-rank lag | Time from durable event-log commit (write-ack) to that event's effect being visible in the materialized rank index | Directly measures the projection lag Component 10 says must be bounded and monitored, not assumed |
| Rank-consistency checks | Periodic comparison of the materialized index's order against the comparator re-evaluated from the authoritative rows | Catches tie-break or comparator-direction bugs (Components 03, 07) before players notice an impossible ordering |
| p99 query latency | Tail latency for top-N, own-rank, and nearby-rank reads | Tail, not average, latency is what a player actually experiences as "the leaderboard feels slow" |
| Ingestion backlog depth | Queued-but-unprocessed event count feeding the projection | Early warning of hot-key pressure (Component 14) or a stalled consumer, before lag becomes player-visible |
- 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