Redis Cheatsheet
Sorted Sets
Use this Redis reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Command table
A sorted set holds unique members each carrying a double-precision score, kept ordered
by score and, for ties, lexicographically by member. It is a skip list plus a hash, which
buys O(log n) insertion with O(1) score lookup. This is the most useful type in Redis.
| Command | What it does | Complexity |
|---|---|---|
ZADD key [opts] score m [score m ...] | Add or update members | O(log n) per member |
ZINCRBY key delta m | Add to a score, creating the member at 0 first | O(log n) |
ZSCORE key m | One score as a string, nil if absent | O(1) |
ZMSCORE key m [m ...] | Several scores (6.2+) | O(n) members |
ZCARD key | Number of members | O(1) |
ZCOUNT key min max | Members in a score range | O(log n) |
ZLEXCOUNT key min max | Members in a lex range, all scores equal | O(log n) |
ZRANK key m [WITHSCORE] | 0-based rank low to high, WITHSCORE is 7.2+ | O(log n) |
ZREVRANK key m [WITHSCORE] | Rank high to low | O(log n) |
ZRANGE key start stop [opts] | The one range command, see below | O(log n + count) |
ZRANGESTORE dst src start stop [opts] | Range straight into a key (6.2+) | O(log n + count) |
ZREM key m [m ...] | Remove members | O(log n) per member |
ZREMRANGEBYRANK key start stop | Remove by rank position | O(log n + count) |
ZREMRANGEBYSCORE key min max | Remove by score range | O(log n + count) |
ZREMRANGEBYLEX key min max | Remove by lex range | O(log n + count) |
ZPOPMIN key [count] | Pop the lowest-scoring members (5.0+) | O(log n × count) |
ZPOPMAX key [count] | Pop the highest-scoring members | O(log n × count) |
BZPOPMIN / BZPOPMAX key [key ...] timeout | Blocking pop, reply names the key | O(log n) |
ZMPOP / BZMPOP | Multi-key pop with COUNT (7.0+) | O(log n × count) |
ZRANDMEMBER key [n [WITHSCORES]] | Random members, negative n allows repeats (6.2+) | O(n) |
ZSCAN key cursor [MATCH p] [COUNT n] | Cursor iterate | O(1) per call |
ZUNIONSTORE dst n k [k ...] [WEIGHTS] [AGGREGATE] | Union into a key | O(n log n) |
ZINTERSTORE dst n k [k ...] [WEIGHTS] [AGGREGATE] | Intersection into a key | O(n log n) |
ZDIFFSTORE dst n k [k ...] | Difference into a key (6.2+) | O(total) |
ZUNION / ZINTER / ZDIFF n k [k ...] | Same, returning the result (6.2+) | O(n log n) |
ZINTERCARD n k [k ...] [LIMIT l] | Intersection size only (7.0+) | O(n×m) |
Scores are IEEE 754 doubles, so integers stay exact only up to 2^53. A unix millisecond timestamp (about 1.7×10^12) is well inside that, but a nanosecond one is not.
ZADD options
ZADD is a full upsert, and its flag block is what makes it safe under concurrency.
| Flag | Effect |
|---|---|
NX | Only add new members, never update an existing score |
XX | Only update existing members, never add |
GT | Update only if the new score is greater (6.2+), adds new members |
LT | Update only if the new score is less (6.2+), adds new members |
CH | Return the count of changed members, not just added ones |
INCR | Treat the score as an increment, return the new score, one member only |
ZADD hu:lb:2026-09 GT CH 10 ada # 1, added ZADD hu:lb:2026-09 GT CH 5 ada # 0, refused, 5 is not greater ZSCORE hu:lb:2026-09 ada # "10" ZADD hu:lb:2026-09 INCR 5 ada # "15", INCR returns the new score ZADD hu:lb:2026-09 NX INCR 5 ada # (nil), NX blocked it, so no score comes back
NX and XX are mutually exclusive, GT/LT/NX are mutually exclusive, and INCR
accepts exactly one score-member pair. GT is the correct way to record a high score
without a read-modify-write race, and ZADD key INCR is identical to ZINCRBY except
that it can carry NX/XX.
Without CH, the integer reply counts only newly added members, so a pure update
returns 0 and that is not a failure.
ZRANGE, the modern form
Since 6.2 one command covers every range query. Learn it and you can ignore the five deprecated ones.
ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
| Modifier | Meaning |
|---|---|
| (none) | start and stop are ranks, 0-based, negatives count from the end |
BYSCORE | They are scores, -inf/+inf allowed, ( prefix means exclusive |
BYLEX | They are members, [ inclusive, ( exclusive, - and + are the extremes |
REV | Reverse the ordering, which also swaps the argument order |
LIMIT offset count | Pagination, only legal with BYSCORE or BYLEX, count -1 means all |
WITHSCORES | Interleave scores into the reply |
ZADD z 1 a 2 b 3 c 4 d > /dev/null ZRANGE z 0 -1 # a b c d, by rank ZRANGE z 0 -1 REV WITHSCORES # d 4 c 3 b 2 a 1 ZRANGE z 0 2 # a b c, stop is INCLUSIVE ZRANGE z "(1" "+inf" BYSCORE LIMIT 0 2 # b c, score > 1, first two ZRANGE z "-inf" "2" BYSCORE # a b, score ≤ 2 ZRANGE z "+inf" "-inf" BYSCORE REV LIMIT 0 2 # d c, note max comes FIRST ZRANGE z "[a" "(c" BYLEX # a b, member ≥ "a" and < "c" ZRANGE z "-" "+" BYLEX # every member ZRANGE z "[user:" "[user:\xff" BYLEX # prefix search, the idiom
With REV the arguments swap. ZRANGE z +inf -inf BYSCORE REV is correct and
ZRANGE z -inf +inf BYSCORE REV returns nothing, silently. That is the single most common
mistake with this command.
BYLEX is only meaningful when every score is identical, because it walks the skip
list assuming member order is the sort order. With mixed scores the result is undefined
rather than an error.
Deprecated older forms, all still supported and all replaceable:
| Deprecated | Modern equivalent |
|---|---|
ZREVRANGE k 0 -1 | ZRANGE k 0 -1 REV |
ZRANGEBYSCORE k min max | ZRANGE k min max BYSCORE |
ZREVRANGEBYSCORE k max min | ZRANGE k max min BYSCORE REV |
ZRANGEBYLEX k min max | ZRANGE k min max BYLEX |
ZREVRANGEBYLEX k max min | ZRANGE k max min BYLEX REV |
They are marked deprecated as of 6.2 and are not going away, so existing code is fine.
Write new code against ZRANGE.
ZRANGESTORE writes a range into another key instead of returning it, which is how you
snapshot a page or trim a leaderboard server side.
ZRANGESTORE hu:lb:top100 hu:lb:2026-09 0 99 REV EXPIRE hu:lb:top100 300
Removing and popping
ZREM hu:lb:2026-09 ada # by member ZREMRANGEBYRANK hu:lb:2026-09 0 -1001 # keep only the top 1000 ZREMRANGEBYSCORE hu:jobs:due "-inf" "$(date +%s)" # drop everything already due ZREMRANGEBYLEX hu:index:names "[a" "(b" # drop a lex prefix ZPOPMIN hu:jobs:due 1 # member and score, lowest first ZPOPMAX hu:lb:2026-09 # the current leader, removed BZPOPMIN hu:q:p1 hu:q:p2 5 # blocks, reply names the key it popped from ZMPOP 2 hu:q:p1 hu:q:p2 MIN COUNT 10 # 7.0+, batch form
ZREMRANGEBYRANK key 0 -1001 is the leaderboard-trim idiom: negative ranks count from the
top, so this deletes everything except the highest-scoring 1000.
Aggregating across sets
ZUNIONSTORE and ZINTERSTORE take an explicit numkeys count, then optional
per-key weights and a combining function.
| Option | Values |
|---|---|
WEIGHTS w [w ...] | One multiplier per input key, default 1 |
AGGREGATE SUM | MIN | MAX | How to combine scores for a shared member, default SUM |
# A 7-day leaderboard from 7 daily ones, all weighted equally ZUNIONSTORE hu:lb:7d 7 hu:lb:d1 hu:lb:d2 hu:lb:d3 hu:lb:d4 \ hu:lb:d5 hu:lb:d6 hu:lb:d7 EXPIRE hu:lb:7d 3600 # Recency-weighted: today counts triple ZUNIONSTORE hu:lb:w 3 hu:lb:d1 hu:lb:d2 hu:lb:d3 WEIGHTS 3 2 1 # Best single day per player, not the sum ZUNIONSTORE hu:lb:best 3 hu:lb:d1 hu:lb:d2 hu:lb:d3 AGGREGATE MAX # Sets count as score 1 per member, so you can mix types ZINTERSTORE hu:scored:active 2 hu:lb:2026-09 hu:active:today AGGREGATE MIN ZINTERCARD 2 hu:lb:2026-09 hu:active:today LIMIT 100 # size only, 7.0+ ZDIFF 2 hu:lb:2026-09 hu:banned WITHSCORES # 6.2+, returns the result
A plain set may be used anywhere these commands take a key, and each of its members
contributes score 1 (multiplied by its weight), which is how you filter a leaderboard by
an unscored membership set. In Cluster mode every key named must hash to the same slot,
so use a hash tag such as hu:{lb}:d1.
Leaderboard
ZINCRBY hu:lb:2026-09 1 "user:1042" # award a point ZRANGE hu:lb:2026-09 0 9 REV WITHSCORES # top 10 ZREVRANK hu:lb:2026-09 "user:1042" WITHSCORE # rank and score, 7.2+ ZCARD hu:lb:2026-09 # total players ZCOUNT hu:lb:2026-09 100 "+inf" # how many above 100
Rank is 0-based, so display rank + 1. Ties break lexicographically by member, which
means alphabetical luck decides order among equal scores. The standard fix is a composite
score that encodes time as a tiebreak, for example points * 10^10 + (2^31 - epoch), kept
inside the 2^53 exact-integer budget.
A neighbours window around one player:
rank=$(redis-cli ZREVRANK hu:lb:2026-09 "user:1042") lo=$(( rank > 2 ? rank - 2 : 0 )) redis-cli ZRANGE hu:lb:2026-09 "$lo" "$(( lo + 4 ))" REV WITHSCORES
Delayed jobs by timestamp
Score by the unix time the job becomes due and the sorted set is a scheduler.
# Schedule for 60 seconds from now ZADD hu:sched:due "$(( $(date +%s) + 60 ))" "job:9f3a" # Poller: claim everything already due, atomically ZRANGE hu:sched:due "-inf" "$(date +%s)" BYSCORE LIMIT 0 100
The read-then-remove version races two pollers against each other. Do it in one script.
-- EVAL <script> 1 hu:sched:due <now> <limit> local due = redis.call('ZRANGE', KEYS[1], '-inf', ARGV[1], 'BYSCORE', 'LIMIT', 0, tonumber(ARGV[2])) if #due > 0 then redis.call('ZREM', KEYS[1], unpack(due)) end return due
unpack on a very large table overflows the Lua stack, so keep the limit in the hundreds
and loop. To make the claim retryable rather than destructive, ZADD each job into an
in-flight set with a deadline score instead of ZREM, and reap anything still there later.
Priority queue, same structure, score is the priority:
ZADD hu:q:pri 1 "urgent-job" 5 "normal-job" 9 "batch-job" ZPOPMIN hu:q:pri # "urgent-job", lowest score wins
HyperLogLog
Not a sorted set, but it lives next door: a fixed-size probabilistic counter of distinct elements. 12KB per key regardless of cardinality, with a standard error of 0.81 percent.
| Command | What it does | Complexity |
|---|---|---|
PFADD key el [el ...] | Add elements, 1 if the estimate changed | O(1) per element |
PFCOUNT key [key ...] | Estimated cardinality, unioning several keys on the fly | O(1), O(n) multi-key |
PFMERGE dst src [src ...] | Union several HLLs into one | O(n) keys |
PFADD hu:uv:2026-09-15 "user:1042" "user:1043" # 1 PFCOUNT hu:uv:2026-09-15 # 2 PFCOUNT hu:uv:2026-09-15 hu:uv:2026-09-14 # union, WITHOUT storing it PFMERGE hu:uv:w37 hu:uv:2026-09-09 hu:uv:2026-09-10 PFCOUNT hu:uv:w37
The trade is stark. A set of 10 million ids costs hundreds of megabytes and gives an exact answer. A HyperLogLog costs 12KB and is within about 0.81 percent. Use it for unique-visitor and unique-event counts where a fraction of a percent does not matter.
Limits: there is no PFREM, you cannot list members, you cannot intersect (only union),
and small cardinalities use a sparse encoding that is smaller than 12KB but is converted
permanently once hll-sparse-max-bytes is crossed. When you need exact counts or the
member list, use a set.