Redis Cheatsheet
Lists
Use this Redis reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Command table
A Redis list is a doubly linked list of listpack nodes (a quicklist). Pushing and
popping at either end is O(1), reaching into the middle is O(n). That single fact decides
every correct use of the type: queues and capped logs, never random access.
| Command | What it does | Complexity |
|---|---|---|
LPUSH key v [v ...] | Prepend, return the new length | O(n) values |
RPUSH key v [v ...] | Append, return the new length | O(n) values |
LPUSHX / RPUSHX key v | Push only if the key already exists | O(1) |
LPOP key [count] | Pop from the head, count is 6.2+ | O(count) |
RPOP key [count] | Pop from the tail | O(count) |
LLEN key | Length, 0 if absent | O(1) |
LRANGE key start stop | Inclusive slice, negatives count from the tail | O(offset+count) |
LINDEX key i | Element at an index, nil if out of range | O(n) |
LSET key i v | Overwrite an existing index, error if out of range | O(n) |
LINSERT key BEFORE/AFTER pivot v | Insert next to the first matching value | O(n) |
LREM key count v | Remove matching elements, see the sign rule | O(n) |
LTRIM key start stop | Keep only that range, discard the rest | O(n removed) |
LPOS key v [RANK r] [COUNT n] [MAXLEN m] | Index of a value (6.0.6+) | O(n) |
RPOPLPUSH src dst | Tail of src to head of dst (superseded by LMOVE) | O(1) |
LMOVE src dst LEFT/RIGHT LEFT/RIGHT | Atomic move, any two ends (6.2+) | O(1) |
LMPOP n k [k ...] LEFT/RIGHT [COUNT c] | Pop from the first non-empty key (7.0+) | O(n+c) |
There is no LDEL by index. Deleting element 5 means LSET to a sentinel then LREM,
which is the usual sign you wanted a sorted set.
Slicing and inspecting
RPUSH hu:log:job:9 a b c d e # (integer) 5 LRANGE hu:log:job:9 0 -1 # a b c d e, -1 is the last index LRANGE hu:log:job:9 0 2 # a b c, stop is INCLUSIVE LRANGE hu:log:job:9 -2 -1 # d e LINDEX hu:log:job:9 0 # a LLEN hu:log:job:9 # 5 LPOP hu:log:job:9 2 # a b, count form pops several
LRANGE's stop is inclusive, unlike almost every slice API you have used. LRANGE k 0
-1 is the whole list, and out-of-range indices are clamped rather than an error.
LPOS finds a value without pulling the list into your client.
RPUSH q a b a c a > /dev/null LPOS q a # 0, the first match LPOS q a RANK -1 # 4, search from the tail LPOS q a RANK 2 # 2, the SECOND match LPOS q a COUNT 0 # 0 2 4, every match, COUNT 0 means all LPOS q z MAXLEN 100 # nil, and it gave up after 100 comparisons
LREM's count argument is signed, which is easy to get backwards:
count | Removes |
|---|---|
> 0 | That many matches, scanning head to tail |
< 0 | That many matches, scanning tail to head |
0 | Every match |
A capped log with LTRIM
LTRIM keeps a range and throws the rest away, which makes a fixed-size ring buffer out
of two commands. The pair is the standard "last N events" structure.
LPUSH hu:events:user:1042 "$(date -u +%FT%TZ) login" LTRIM hu:events:user:1042 0 99 # keep the newest 100, drop the rest LRANGE hu:events:user:1042 0 9 # the 10 most recent
Push to the head and trim 0 N-1 so index 0 is always newest. Run the LTRIM on every
push, not periodically, since it is O(number removed) and that is normally 1. Wrap the
two in a pipeline or MULTI so a crash cannot leave the list unbounded, and set a TTL on
the key so an abandoned log eventually disappears.
Blocking commands
The blocking variants are what make a list a real queue. The consumer sleeps inside Redis instead of polling.
| Command | What it does |
|---|---|
BLPOP key [key ...] timeout | Block until the head of some key is poppable |
BRPOP key [key ...] timeout | Same from the tail |
BLMOVE src dst LEFT/RIGHT LEFT/RIGHT timeout | Blocking LMOVE (6.2+) |
BRPOPLPUSH src dst timeout | Older form of BLMOVE src dst RIGHT LEFT |
BLMPOP timeout n k [k ...] LEFT/RIGHT [COUNT c] | Blocking LMPOP (7.0+) |
BLPOP hu:q:high hu:q:low 5 # 1) "hu:q:high" 2) "job-payload" BLPOP hu:q:high hu:q:low 0 # 0 means block forever
Facts that matter:
- The reply names which key it popped from, so a multi-key BLPOP gives you strict
priority. Keys are checked left to right, so hu:q:high always drains first.
- timeout is a double since 6.0, so BLPOP q 0.05 is legal.
- On timeout the reply is nil (a nil array in RESP2).
- Inside MULTI a blocking command does not block. It behaves like the
non-blocking version and returns nil immediately.
- A blocked connection consumes a connection slot. Size maxclients for your worker
count.
- Blocked clients are served in FIFO order, so the longest waiting consumer wins.
Simple queue, and why it loses jobs
# Producer LPUSH hu:q:email '{"to":"ada@example.com","tpl":"welcome"}' # Consumer BRPOP hu:q:email 0
LPUSH at the head plus BRPOP at the tail is FIFO and takes two commands total. It is
the right answer for work you can afford to lose, and the wrong answer otherwise, because
the moment BRPOP returns, the job exists only in the worker's memory. A crash, a
deploy, or an OOM kill and it is gone with no trace.
Reliable queue
The fix is to never remove a job without simultaneously recording that someone took it.
BLMOVE does both in one atomic step.
# Producer LPUSH hu:q:email "$job" # Consumer: atomically claim into a per-worker processing list job=$(redis-cli BLMOVE hu:q:email "hu:q:email:proc:$WORKER" RIGHT LEFT 0) # ... do the work ... # Acknowledge by removing exactly that payload redis-cli LREM "hu:q:email:proc:$WORKER" 1 "$job"
Recovery is now possible, because a crashed worker leaves its jobs visible in its processing list.
# Requeue a dead worker's in-flight jobs, oldest first while redis-cli LMOVE "hu:q:email:proc:$DEAD" hu:q:email LEFT RIGHT | grep -q .; do :; done
The remaining caveats are real and worth stating plainly:
- Delivery is at least once. A worker that finishes the job then dies before LREM
will have the job replayed, so handlers must be idempotent.
- LREM matches by value, so two identical payloads are indistinguishable. Put a
unique id in every job.
- Nothing detects a stuck worker. You need a separate reaper comparing a per-job
heartbeat key against a deadline, and you have to decide how long is too long.
- There are no per-job retry counts, no delays, and no dead-letter queue. You build all
three or you outgrow the pattern.
When to stop. If you need consumer groups, acknowledgement tracking, pending-entry
inspection, or automatic claim of abandoned work, that is exactly what streams provide
natively. Reach for XADD plus XREADGROUP rather than reinventing it on lists.
Lists versus the alternatives
| Need | Use | Why not a list |
|---|---|---|
| Fire-and-forget fanout | Pub/Sub | A list is point to point, one consumer wins |
| Consumer groups, acks, replay | Stream | Lists have no ack and no history |
| Priority by number | Sorted set | ZPOPMIN pops the lowest score |
| Delay until a timestamp | Sorted set | Score by unix time, poll with ZRANGEBYSCORE |
| Random access by index | Hash or sorted set | LINDEX is O(n) |
| Deduplicated membership | Set | Lists happily hold duplicates |
Keep an eye on list-max-listpack-size (128 by default). Beyond it each node becomes a
separate allocation, and list-compress-depth can LZF-compress interior nodes when you
have very long lists that are only touched at the ends.