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.
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.
Core operations — ZADD and the score
leaderboards.md.txt · board.exA 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.
Rank vs reverse rank
leaderboards.md.txt · Rank vs Reverse RankA 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.
Players around a rank
leaderboards.md.txt · Players Around a RankTo 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.
Tiebreaking with composite scores
leaderboards.md.txt · TiebreakingWhen 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.
Pagination & efficient player info
leaderboards.md.txt · PaginationA 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.
Variations — time-windowed & aggregating
leaderboards.md.txt · Time-Windowed, AggregatingA “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.
The pattern, applied — codemojex's board
echo/apps/codemojex · board.ex · scoring.exThe 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.
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
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.
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.
The queue's score-ordered work
A leaderboard is a generic sorted-set ranking, not the queue protocol. Where EchoMQ ranks for real is the
Queue pillar — work ordered by a packed schedule and priority score in the emq:{q}:
keyspace, the same score-order idea.
How a board survives a restart
The board is a derived view of what was scored. The persistence floor is the durable substrate that rebuilds the volatile board after a restart — the durability dial beneath the bus.
The three dives
write → read → updateEach 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.
ZADD and ZREVRANK
The core: ZADD writes a score, ZREVRANK reads a 0-based rank from the
top. The write codemojex's Board.record/3 makes.
Top-N and around-me
Two reads over one order: ZREVRANGE 0 N-1 WITHSCORES for the top-N (what
Board.top/2 runs), and the around-me band.
The score-update path
ZADD overwrites (the best-of write), ZINCRBY accumulates; the rank is
recomputed on read, never stored.
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.
References
sources · relatedSources
- 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/2runs. - 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
- R4.05.1 · ZADD and ZREVRANKscore in, rank out
- R4.05.2 · Top-N and around-methe two read shapes
- R4.05.3 · The score-update pathoverwrite, accumulate, rank on read
- R4 · Time, Delay & Prioritythe chapter
- R4.03 · Composite priority scoresthe same score-packing, there for ties
- /echomq · the Queue pillarthe queue's score-ordered work in depth