Redis Cheatsheet
Streams
Use this Redis reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Command table
A stream is an append-only log of entries, each with a monotonic ID and a flat field-value map. Unlike pub/sub it persists, and unlike a list it supports consumer groups with per-message acknowledgement. This is Redis's Kafka-shaped type.
| Command | What it does | Complexity |
|---|---|---|
XADD key [NOMKSTREAM] [trim] id f v [f v ...] | Append an entry, return its ID | O(1) |
XLEN key | Number of entries currently stored | O(1) |
XRANGE key start end [COUNT n] | Entries in an ID range, oldest first | O(log n + count) |
XREVRANGE key end start [COUNT n] | Same, newest first, note the swapped args | O(log n + count) |
XREAD [COUNT n] [BLOCK ms] STREAMS k [k ...] id [id ...] | Read forward from an ID | O(log n + count) |
XDEL key id [id ...] | Delete entries by ID | O(1) per ID |
XTRIM key MAXLEN|MINID [~|=] n [LIMIT c] | Trim the log | O(n removed) |
XAUTOCLAIM / XCLAIM | Reassign pending entries to another consumer | O(1) per entry |
XSETID key id [ENTRIESADDED n] [MAXDELETEDID id] | Force the last-generated ID | O(1) |
XINFO STREAM|GROUPS|CONSUMERS | Introspection | O(1) |
Entry IDs
An ID is <milliseconds>-<sequence>, both 64-bit unsigned, and it strictly increases.
* asks the server to generate one, which is what you want in production.
XADD hu:events '*' user 1042 action login # "1789531888747-0" XADD hu:events 5-1 a 1 # explicit ID, legal XADD hu:events 5-2 a 2 # legal, sequence bumped XADD hu:events 4-1 a 3 # (error) ERR The ID specified in XADD is equal or smaller # than the target stream top item XADD hu:events NOMKSTREAM '*' a 1 # (nil) if the stream does not exist yet XADD hu:events '5-*' a 1 # 7.0+, server picks the sequence
| Special ID | Where | Means |
|---|---|---|
* | XADD | Generate from the server clock |
- | XRANGE | The smallest possible ID |
+ | XRANGE | The largest possible ID |
$ | XREAD | Only entries added after this call |
> | XREADGROUP | Entries never delivered to any consumer in the group |
0 | XREADGROUP | This consumer's own pending entries, for recovery |
(id | XRANGE, XREAD | Exclusive, so start just after id (6.2+) |
Because IDs come from the clock, a backwards clock jump makes XADD fail rather than
produce an out-of-order log.
Reading without a group
XLEN hu:events XRANGE hu:events - + COUNT 10 # oldest 10 XREVRANGE hu:events + - COUNT 10 # newest 10, args reversed XRANGE hu:events "(5-1" + # everything strictly after 5-1 XRANGE hu:events 1789531000000 1789532000000 # a time window, ms prefix is enough XREAD COUNT 100 STREAMS hu:events 0 # from the beginning XREAD BLOCK 0 STREAMS hu:events '$' # tail -f, block forever XREAD BLOCK 5000 COUNT 10 STREAMS hu:a hu:b '$' '$' # several streams at once
Pagination is a loop of XRANGE with an exclusive start, carrying the last ID you saw:
last="-" while :; do # after the first page, use ( to exclude the ID you already handled redis-cli XRANGE hu:events "$last" + COUNT 100 # set last="($id_of_last_entry)" and break when a page comes back short done
XREAD with $ is a plain fan-out tail. Every reader independently gets everything, there
is no acknowledgement, and nothing tracks position for you. That is fine for a live feed
and wrong for work distribution.
Trimming
Streams grow forever unless you trim them. Trimming is an argument to XADD, so it costs
nothing extra on the hot path.
XADD hu:events MAXLEN '~' 10000 '*' a 1 # keep ROUGHLY the newest 10000 XADD hu:events MAXLEN = 10000 '*' a 1 # keep EXACTLY 10000 XADD hu:events MINID '~' "$(( ($(date +%s) - 86400) * 1000 ))" '*' a 1 XTRIM hu:events MAXLEN '~' 10000 XTRIM hu:events MINID '~' 1789531000000 XTRIM hu:events MAXLEN '~' 10000 LIMIT 500 # cap the work per call, 6.2+
Always use ~. The tilde means "at least this many, trimmed on macro-node
boundaries", so Redis drops whole radix-tree nodes instead of walking entries. = is exact
and O(n removed), which is a latency spike waiting to happen on a large trim. MINID trims
by age rather than count and is the better fit when entry rate varies.
XDEL removes an entry by ID but does not reclaim its space immediately, and it does
not remove the entry from any consumer group's pending list. XLEN counts live entries
while XINFO STREAM's entries-added counts everything ever appended.
Consumer groups
A consumer group gives you the thing lists and pub/sub cannot: each entry goes to exactly one consumer in the group, and it stays in that consumer's Pending Entries List (PEL) until acknowledged.
| Command | What it does |
|---|---|
XGROUP CREATE key group id [MKSTREAM] [ENTRIESREAD n] | Create a group starting at id |
XGROUP CREATECONSUMER key group consumer | Pre-create a consumer |
XGROUP DELCONSUMER key group consumer | Drop a consumer, returns its pending count |
XGROUP SETID key group id | Rewind or fast-forward the group |
XGROUP DESTROY key group | Delete the group and its PEL |
XREADGROUP GROUP g c [COUNT n] [BLOCK ms] [NOACK] STREAMS key id | Read as a consumer |
XACK key group id [id ...] | Acknowledge, removing entries from the PEL |
XPENDING key group [IDLE ms] [start end count [consumer]] | Inspect the PEL |
XCLAIM key group consumer min-idle-time id [...] | Take specific entries |
XAUTOCLAIM key group consumer min-idle-time start [COUNT n] [JUSTID] | Scan and take (6.2+) |
# Create the group and the stream in one step. 0 replays history, $ starts at the tail. XGROUP CREATE hu:events workers 0 MKSTREAM # Each worker uses a stable, unique consumer name XREADGROUP GROUP workers worker-1 COUNT 10 BLOCK 5000 STREAMS hu:events '>' # ... process ... XACK hu:events workers 1789531888747-0
> means "entries no one has been given yet". Any other ID, and 0 in particular, means
"re-read my own unacknowledged entries", which is exactly what a restarting worker does
first.
# Worker startup: drain my own PEL before taking new work XREADGROUP GROUP workers worker-1 COUNT 100 STREAMS hu:events 0 XREADGROUP GROUP workers worker-1 COUNT 10 BLOCK 5000 STREAMS hu:events '>'
NOACK skips the PEL entirely, turning the group into at-most-once delivery. It is faster
and it loses messages on a crash.
XGROUP CREATE on a missing stream errors with NOGROUP unless you pass MKSTREAM.
Pending entries and recovery
# Summary form: count, min ID, max ID, then per-consumer counts XPENDING hu:events workers # 1) (integer) 2 # 2) "1789531888747-0" # 3) "1789531888751-0" # 4) 1) 1) "worker-1" # 2) "2" # Extended form: one row per entry, with idle time and delivery count XPENDING hu:events workers - + 10 # 1) 1) "1789531888747-0" # 2) "worker-1" # 3) (integer) 74123 <- ms since it was last delivered # 4) (integer) 1 <- delivery attempts XPENDING hu:events workers IDLE 60000 - + 10 # only stale ones XPENDING hu:events workers - + 10 worker-1 # one consumer
The delivery count is your retry counter. Anything above a threshold is a poison message,
so route it to a dead-letter stream and XACK the original.
XAUTOCLAIM is the reaper. It scans the PEL from a cursor and reassigns anything idle
longer than the threshold.
# Take over everything idle for more than 60s, in pages XAUTOCLAIM hu:events workers worker-2 60000 0 COUNT 100 # 1) "0-0" <- next cursor, 0-0 means done # 2) 1) 1) "1789531888751-0" # 2) 1) "user" 2) "1042" # 3) (empty array) <- IDs deleted from the stream, dropped from the PEL XAUTOCLAIM hu:events workers worker-2 60000 0 COUNT 100 JUSTID # IDs only XCLAIM hu:events workers worker-2 0 1789531888751-0 # one specific entry
XCLAIM takes explicit IDs and supports IDLE, TIME, RETRYCOUNT, FORCE (claim an
entry not currently in the PEL), and JUSTID (which does not bump the delivery count).
Deleting a consumer hands its work back for someone else to claim:
XGROUP DELCONSUMER hu:events workers worker-1 # returns its pending count
Introspection
XINFO STREAM hu:events # length, entries-added, last-generated-id, first/last entry XINFO STREAM hu:events FULL # includes every group's full PEL, expensive XINFO GROUPS hu:events # per group: consumers, pending, last-delivered-id, lag XINFO CONSUMERS hu:events workers # per consumer: pending, idle, inactive
lag in XINFO GROUPS is the number of entries the group has not read yet, which is your
backlog metric. It reports nil when the stream has been trimmed or XDEL'd in a way that
makes the count unknowable, so alert on pending and consumer idle as well.
Stream, list, or pub/sub
| Pub/Sub | List queue | Stream + group | |
|---|---|---|---|
| Persistence | None, fire and forget | Until popped | Full log, trimmed on your terms |
| Offline subscriber | Loses everything | Fine, work waits | Fine, reads its backlog |
| Delivery | To every subscriber | To one popper | One consumer per group, per entry |
| Acknowledgement | No | No, LREM by value at best | Native, XACK plus a PEL |
| Retry after a crash | Impossible | Manual processing list | XPENDING plus XAUTOCLAIM |
| Multiple independent readers | Yes, all get all | No, they compete | Yes, one group each |
| Replay history | No | No | Yes, XRANGE or XGROUP SETID |
| Delivery count per message | No | No | Yes |
| Cost | Cheapest | Cheap | Memory for retained entries |
The decision is short. Live notifications where a missed message does not matter: pub/sub.
Simple work distribution you are willing to hand-roll recovery for: a list with BLMOVE.
Anything that needs acknowledgement, retries, replay, or several independent consumer
groups over the same events: streams. Delivery is still at least once, so handlers must
be idempotent, and Redis is still not a durable message broker, so an unreplicated node
loss loses whatever was not persisted.