ZINCRBY and Leaderboards

A sorted set in Redis keeps a group of unique members ordered by a floating-point score, and ZINCRBY is the command that atomically adds to (or subtracts from) a member’s score in one step. This single feature makes sorted sets the natural data structure for leaderboards, vote counts, and any ranking system where scores change constantly and concurrently. Because the increment happens as one atomic operation on Redis’s single thread, you never have to worry about two clients racing to update the same player’s score and clobbering each other’s work.

Overview: How ZINCRBY and Sorted Sets Work

Internally, a Redis sorted set (type zset) is backed by two structures kept in sync: a hash table mapping each member to its score for O(1) score lookups, and a skip list that keeps members ordered by score for fast range queries. A skip list is a linked structure with multiple “levels” of pointers, which lets Redis find, insert, or remove an element in O(log N) time on average without needing a balanced tree. Every command that touches a sorted set’s ordering — ZADD, ZINCRBY, ZRANGE, ZRANK — pays that O(log N) cost (or O(log N) + M for range results).

ZINCRBY key increment member reads the member’s current score from the hash table, adds increment to it, and reinserts the member at its new position in the skip list — all inside a single command execution. Because Redis is single-threaded, no other command can run in the middle of that read-modify-write; two clients calling ZINCRBY on the same player at nearly the same instant will both apply their full increment, in some serial order, with no lost updates. This is exactly what you want for a leaderboard: “add 10 points” from a thousand concurrent game clients should always add up to the correct total, never silently drop an update.

If the member does not already exist in the sorted set, ZINCRBY creates it and treats its starting score as 0, so the new score is simply the increment you passed. This means you never need to check whether a player already has a score before incrementing — the first ZINCRBY call for a new player both creates and scores them correctly.

Syntax

ZINCRBY key increment member
  • key — the name of the sorted set (the leaderboard).
  • increment — a number (integer or floating-point) to add to the member’s score. Pass a negative number to subtract, e.g. -50.
  • member — the element whose score is being changed (e.g. a player or user id). Created with a starting score of 0 plus increment if it doesn’t already exist.

ZINCRBY replies with the member’s new score as a bulk string, so your application always knows the resulting total without a follow-up ZSCORE call.

Command Purpose Time Complexity
ZADD Set (or overwrite) a member’s score O(log N) per added element
ZINCRBY Atomically add to a member’s score O(log N)
ZSCORE Read a member’s current score O(1)
ZRANGE / ZREVRANGE List members in score order O(log N + M), M = elements returned
ZRANK / ZREVRANK Get a member’s position in the order O(log N)

Examples

Example 1: Incrementing a single player’s score

ZADD leaderboard:game1 100 "player:1"
ZINCRBY leaderboard:game1 50 "player:1"
ZSCORE leaderboard:game1 "player:1"

Output:

(integer) 1
"150"
"150"

ZADD creates the sorted set with player:1 starting at a score of 100, returning (integer) 1 because one new member was added. ZINCRBY then adds 50 to that score in one atomic step and returns the new total, "150", as a bulk string. The follow-up ZSCORE confirms the same value is now stored.

Example 2: Multiple players, negative increments, and ranking

ZADD leaderboard:global 500 "alice" 300 "bob"
ZINCRBY leaderboard:global 120 "alice"
ZINCRBY leaderboard:global -50 "bob"
ZINCRBY leaderboard:global 75 "carol"
ZRANGE leaderboard:global 0 -1 WITHSCORES
ZREVRANGE leaderboard:global 0 -1 WITHSCORES

Output:

(integer) 2
"620"
"250"
"75"
1) "carol"
2) "75"
3) "bob"
4) "250"
5) "alice"
6) "620"
1) "alice"
2) "620"
3) "bob"
4) "250"
5) "carol"
6) "75"

Alice and bob start the leaderboard. ZINCRBY raises alice’s score by 120 (to 620), lowers bob’s by 50 with a negative increment (to 250), and creates carol on the fly with a starting score of 75, since she didn’t exist before. ZRANGE ... WITHSCORES lists everyone from lowest to highest score (the default sorted-set order), while ZREVRANGE ... WITHSCORES lists them highest first — the order you actually want for a leaderboard display.

Example 3: A daily leaderboard with rank lookup and expiration

ZADD leaderboard:daily:2026-08-10 1200 "user:42" 950 "user:7" 1500 "user:19"
ZINCRBY leaderboard:daily:2026-08-10 300 "user:7"
ZREVRANK leaderboard:daily:2026-08-10 "user:7"
ZSCORE leaderboard:daily:2026-08-10 "user:7"
EXPIRE leaderboard:daily:2026-08-10 86400
TTL leaderboard:daily:2026-08-10

Output:

(integer) 3
"1250"
(integer) 1
"1250"
(integer) 1
(integer) 86400

Three players seed the daily leaderboard. After user:7 earns 300 more points, ZINCRBY reports their new total of 1250. ZREVRANK returns their 0-based rank counting from the top score — (integer) 1 means they’re in 2nd place, behind user:19‘s 1500. Finally, EXPIRE puts a 24-hour TTL on the whole leaderboard key so a “daily” leaderboard actually resets itself instead of accumulating forever, and TTL confirms the 86400 seconds is set.

How It Works Step by Step

When Redis receives ZINCRBY key increment member, it performs the following, entirely on the single command-processing thread before any other command is allowed to run:

  1. Look up key in the keyspace. If it doesn’t exist, an empty sorted set is created.
  2. Look up member in the sorted set’s internal hash table (O(1)). If it’s not there, its current score is treated as 0.
  3. Compute new_score = current_score + increment.
  4. Remove the member from its old position in the skip list (if it had one), then reinsert it at the position matching new_score — this is the O(log N) step.
  5. Update the hash table entry so future ZSCORE/ZINCRBY lookups see the new score.
  6. Return the new score to the client as a bulk string.

Because steps 2 through 5 all happen inside one command with no other client’s commands interleaved, there’s no window where a second ZINCRBY could read a stale score. Compare this to doing it yourself with ZSCORE to read, adding in your application code, then ZADD to write back — two round trips with a gap in between where another client’s update can be silently overwritten.

Common Mistakes

Mistake 1: Using ZADD to add points instead of ZINCRBY

ZADD leaderboard:game2 150 "player:1"
ZADD leaderboard:game2 10 "player:1"
ZSCORE leaderboard:game2 "player:1"

Output:

(integer) 1
(integer) 0
"10"

ZADD always sets a member’s score to the value you pass — it never adds to the existing score. The second call here doesn’t add 10 to player 1’s existing 150; it replaces it, and the returned (integer) 0 (no new member added, since it already existed) is an easy-to-miss clue that something silently overwrote instead of incrementing. The player’s earlier 150 points are gone. Use ZINCRBY whenever the intent is “add to the current score”:

ZADD leaderboard:game2 150 "player:1"
ZINCRBY leaderboard:game2 10 "player:1"
ZSCORE leaderboard:game2 "player:1"

Output:

(integer) 1
"160"
"160"

Mistake 2: Treating type errors as impossible

SET leaderboard:bad "not a sorted set"
ZINCRBY leaderboard:bad 10 "player:1"

Output:

OK
(error) WRONGTYPE Operation against a key holding the wrong kind of value

Every Redis key has exactly one type. If leaderboard:bad was accidentally created as a plain string (say, by a typo’d SET somewhere else in your codebase), any sorted-set command against it fails with WRONGTYPE instead of silently doing something reasonable. Always use distinct, namespaced key names for different data types (e.g. keys under leaderboard: only ever hold sorted sets) and handle this error in application code rather than assuming a key always holds what you expect.

Mistake 3: Never expiring time-boxed leaderboards

A “weekly” or “daily” leaderboard that never gets a TTL just keeps accumulating scores forever, quietly wasting memory and showing stale rankings once the period is over. As shown in Example 3, call EXPIRE right after creating a periodic leaderboard key so Redis reclaims it automatically once the period ends, instead of relying on application code to remember to delete it.

Best Practices

  • Use ZINCRBY, not a ZSCORE-then-ZADD pair, whenever you’re adding to an existing score — it’s both atomic and one round trip instead of two.
  • Namespace leaderboard keys clearly and consistently, e.g. leaderboard:global, leaderboard:daily:2026-08-10, so different boards (and different data types) never collide.
  • Put a TTL on time-boxed leaderboards with EXPIRE so they clean themselves up; don’t rely on a cron job to delete them.
  • Use ZREVRANGE/ZREVRANK (highest score first) for player-facing leaderboard displays, since sorted sets are ordered lowest-to-highest by default.
  • Prefer negative increments over separate “decrement” logic — ZINCRBY key -10 member subtracts cleanly using the same command.
  • For very large leaderboards, favor ZRANGE/ZREVRANGE with explicit start/stop indexes (e.g. top 10) over pulling the whole set — each returned element costs extra time, so scanning the full set is wasteful when you only display a page at a time.

Practice Exercises

  • Create a sorted set leaderboard:quiz1 and give three players (e.g. user:a, user:b, user:c) starting scores of your choice with ZADD. Then use ZINCRBY to award points for two more “rounds” and use ZREVRANGE ... WITHSCORES to confirm the final standings.
  • Pick one player from the exercise above and use ZINCRBY with a negative increment to dock 25 points. Confirm with ZSCORE that the score dropped by exactly 25, not that it got reset.
  • Build a small “today’s leaderboard” key, set a TTL of 60 seconds on it with EXPIRE, and check TTL immediately after. Then work out (no need to actually wait) what a plain SET on that same key (instead of ZINCRBY) would do to the TTL you just set.

Summary

  • ZINCRBY key increment member atomically adds increment to a member’s score in a sorted set, creating the member with that score if it didn’t exist — O(log N) time.
  • Because Redis is single-threaded, ZINCRBY has no read-modify-write race window, unlike a manual ZSCORE-then-ZADD pair.
  • Sorted sets are the right structure for leaderboards because members stay ordered by score automatically, unlike a plain set.
  • ZADD sets a score outright and will overwrite an existing one — use it to seed or reset, not to add points.
  • Calling any sorted-set command on a key holding a different type raises a WRONGTYPE error.
  • Use ZREVRANGE/ZREVRANK for highest-score-first display, and put a TTL on time-boxed leaderboards with EXPIRE.