jonnify·redis /redis-patterns/time-delay-priority/leaderboards

R4.05 · Time, Delay & Priority — module hub

Leaderboards (the sorted set as a ranking).

A sorted set is a leaderboard: store each player's score, and rank, top-N, and around-me queries fall out of the score order — no rank is stored; the rank is computed on read in O(log N).

Grounding: echo/apps/codemojex · Codemojex.Board · Codemojex.Scoring — one sorted set per game (cm:<game>:board), each player's best linear total written with ZADD, the top-N read with ZREVRANGE … WITHSCORES.

One game's board, eight players by best linear score. Select a player to light their row and read their rank — the rank is the position in the order, not a stored field.
0—— 1—— 2—— 3—— 4—— 5—— 6—— 7——
readout · Select a player. The board is sorted by score, high to low; a player's ZREVRANK is their row index from the top, with rank 0 the leader.

The leader is vega at 540 (rank 0); the lowest score is nori at 180 (rank 7). The rank is read from the order — no row carries a stored rank.

§1

Core operations — ZADD and the score

leaderboards.md.txt · board.ex

A sorted set stores members with associated scores and maintains order by score automatically. The leaderboard is one sorted set; each member is a player, each score is that player's points. ZADD is the whole write — a member and a score — and re-running it on an existing member overwrites the score and re-places it in the order. A single score is read with ZSCORE, which is O(1).

ZADD leaderboard 540 "vega"      # write a player's score
ZADD leaderboard 510 "mango"     # re-running ZADD overwrites and re-places
ZSCORE leaderboard "vega"        # => 540  (one player's score, O(1))

The score is the entire ordering. Reading the set high-to-low gives the leader at the top; reading low-to-high gives the trailing player. Nothing else indexes the board. This is the move Codemojex.Board.record/3 makes: it folds the player's best base (new_base = max(old, base)) and writes it straight to the board with Cmd.zadd("cm:<game>:board") |> Cmd.score(new_base, player).

The score is the write, and the order is free: a sorted set keeps its members ranked by score so nothing has to sort the board by hand.

§2

Rank vs reverse rank

leaderboards.md.txt · Rank vs Reverse Rank

A rank is a player's position in the order. Redis reads it from each end. ZRANK counts low-to-high — the lowest score is rank 0. ZREVRANK counts high-to-low — the highest score is rank 0. For a board where higher is better, ZREVRANK is the one to read: the leader sits at rank 0, the runner-up at rank 1. A rank is 0-based, so “rank 0” is first place. The two are mirrors of one position in the same order; reading either end never changes the set.

ZREVRANK leaderboard "vega"      # => 0   (top of the order)
ZRANK    leaderboard "vega"      # => 7   (the same position from the bottom)

Notes on ValkeyA sorted set keeps members ordered by score; ZADD adds or updates one member and ZREVRANK reads a 0-based rank, both in O(log N) — neither changes the others' positions except where a new score moves the written member past them — valkey.io/topics/data-types.

§3

Players around a rank

leaderboards.md.txt · Players Around a Rank

To show a player their standing against nearby competitors, read a window of positions either side of their own. First find the player's rank, then read the rank window around it.

ZREVRANK leaderboard "sora"             # say it returns 50
ZREVRANGE leaderboard 45 55 WITHSCORES   # ranks 45–55: sora with five either side

The same two reads serve any “around-me” view: ZREVRANK to locate the player's position, then a rank-range read centred on it. The window is a slice of the existing order; the board is unchanged. codemojex's read side is the head-read of this family — Codemojex.Board.top/2 runs ZREVRANGE … 0 (n-1) WITHSCORES for the top-N; an around-me band is the same order read with different bounds.

Around-me is two reads of one order: locate the rank, then read the band centred on it. No stored rank is needed to answer it.

§4

Tiebreaking with composite scores

leaderboards.md.txt · Tiebreaking

When two players share a score, a plain sorted set falls back to comparing the members lexicographically — not the order a ranking should hold. A sorted set has no secondary sort key, so encode both the points and the tiebreaker into a single score. For a score achieved at a timestamp, pack the points into the high digits and the inverted timestamp into the low digits.

composite = points * 10000000000 + (MAX_TIMESTAMP - timestamp)
# higher points dominate; for a tie, the earlier timestamp ranks higher

Higher points always dominate; for equal points, the earlier timestamp ranks higher because it is subtracted from a maximum. The points are recovered by integer division. This is the same score-packing idea the priority queue uses to order equal-priority jobs by arrival, covered in R4.03 · Composite priority scores. One score carries two axes, the high digits dominating the low.

§5

Pagination & efficient player info

leaderboards.md.txt · Pagination

A large board is read a page at a time — each page is a rank range. ZCARD returns the member count, which gives the page count. To read a player's rank and score together in one network round trip, pipeline the two reads.

ZREVRANGE leaderboard 0 19 WITHSCORES    # page 1: ranks 1–20
ZREVRANGE leaderboard 20 39 WITHSCORES   # page 2: ranks 21–40
ZREVRANK  leaderboard "sora"           # pipeline: rank and …
ZSCORE    leaderboard "sora"           # … score, one round trip

Each page is the same head-read, offset down the order; a rank and a score come back together in one round trip when the two reads are pipelined.

§6

Variations — time-windowed & aggregating

leaderboards.md.txt · Time-Windowed, Aggregating

A “this week” or “today” board is a separate sorted set under a time-keyed name, given a TTL so it expires on its own. Several windows combine with ZUNIONSTORE, whose AGGREGATE option chooses how scores merge — SUM for total points across days, MAX for a personal best, MIN for a best time. Redis 6.2 folded the range commands into a unified ZRANGE with a REV option, so ZRANGE … REV reads the same top-N as the older ZREVRANGE; the ordering is identical, only the spelling changed.

§7

The pattern, applied — codemojex's board

echo/apps/codemojex · board.ex · scoring.ex

The pattern lands twice: the textbook sorted-set leaderboard, and the one codemojex runs. codemojex is a Telegram emoji-guessing game on the same stack. Its competitive state lives in Valkey, and the leaderboard is exactly this pattern, in the real code. The score is the linear total from Codemojex.Scoring — points = 100 - 20 * d per emoji at distance d, summed over six positions, out of 600. The board is one sorted set per game, keyed cm:<game>:board: Codemojex.Board.record/3 folds the player's best base and writes it with ZADD, and Codemojex.Board.top/2 reads the top-N with ZREVRANGE … WITHSCORES. The rank is never stored.

echo/apps/codemojex/lib/codemojex/board.ex · the per-game board, ZADD write & ZREVRANGE readverbatim
defp k(game, suffix), do: "cm:" <> game <> ":" <> suffix

# record/3 — fold the best base, ZADD it to the board
new_base = max(old, base)
Cmd.zadd(k(game, "board")) |> Cmd.score(new_base, player) |> Wire.run(conn)   ← ZADD cm:<game>:board score player

# top/2 — the top-N, highest first, scores attached
Cmd.zrevrange(k(game, "board"), 0, n - 1) |> Cmd.withscores() |> Wire.run(Bus.conn())   ← ZREVRANGE … 0 (n-1) WITHSCORES
the pattern

A sorted set ranks players by score in O(log N), with the rank computed on read and never stored. ZADD writes a score; ZREVRANGE reads the top-N; ZREVRANK reads a player's position.

its codemojex application

Codemojex.Board keeps one sorted set per game (cm:<game>:board), writes each player's best linear total with ZADD (Cmd.zadd/1 + Cmd.score/3), and reads the top-N with ZREVRANGE … WITHSCORES (Board.top/2). The board stores scores; the rank is the order.

A leaderboard never stores a rank; it stores scores and reads the order. codemojex's board is a sorted set per game with each player's best linear score — the rank is whatever the order says on read.

§8

The three dives

write → read → update

Each dive takes one move of the leaderboard, in the arc write → read → update: the score in and the rank out, the two read shapes over the order, and how a score change re-places a member while the rank stays computed on read. Read them in order.

After R4.05, the chapter continues with the workshop — scheduling codemojex's notification and digest jobs. Return to the Time, Delay & Priority chapter to continue, or open Score as meaning for the score as the semantic axis.

§9

References

sources · related

Sources

  • Redis — Sorted sets — the score-ordered structure a leaderboard is: members held in order of a numeric score.
  • Redis — ZADD — add or update a player's score on the board; the whole write.
  • Redis — ZREVRANGE — read the top-N, highest scores first; the read codemojex's Board.top/2 runs.
  • Redis — ZREVRANK — a member's rank, 0-based from the top, computed on read.
  • Redis — ZINCRBY — accumulate a score in place, with no lost update under concurrency.

Related in this course