skip to content

Pub/Sub & Streams

You will learn both Redis messaging models: fire-and-forget pub/sub channels and the persistent, consumer-group-aware Streams log. Interviewers ask this to check you know which delivery guarantees each one gives — and when you should reach for a real broker instead.

part ofRedisoverview, primer and where to startread it →
on this pageshow

explore

questions

21

Redis offers both XRANGE and XREAD for getting entries out of a stream. When would you reach for each?

level: juniorimportance: must knowfreq 50%

answer

  1. XRANGE = history query, inclusive, never waits
  2. XREAD = tail, exclusive of the given ID, can BLOCK
  3. XREVRANGE + - COUNT n = newest n
  4. Reading never removes entries
  5. `$` = only new; `0` = from the beginning

basics

~20 s

XRANGE is a history query: give it a start and end ID and it returns that slice in order, always immediately. XREAD is a consumer: you give it the last ID you saw and it returns only newer entries, and with BLOCK it waits for entries that have not arrived yet.

solid answer

~60 s

**`XRANGE key start end [COUNT n]`** — a **range query over stored history**. Boundaries are IDs (or `-` / `+` for the extremes), the range is **inclusive** on both ends, results come back in ascending ID order, and the call returns immediately with whatever exists. `XREVRANGE` is the same in descending order with the boundaries reversed — the natural way to fetch the newest N entries. Use it for browsing, backfills, replaying a time window, or paging a UI. **`XREAD [COUNT n] [BLOCK ms] STREAMS key ... id ...`** — a **consumer-style tail**. You pass the last ID you have processed and get entries **strictly greater** than it — exclusive, unlike XRANGE. It can read several streams in one call. With `BLOCK ms` it waits for new data instead of returning an empty reply, and `$` means "only entries added after this call is issued". Use it to follow a stream in near real time. Both are *stateless on the server for plain reads*: neither tracks a position for you and neither removes entries. Tracking the last ID is the client's job.

code

text · 10 lines
text
> XRANGE orders - + COUNT 2
1) 1) "1699999999999-0"
   2) 1) "customer"
      2) "42"
2) 1) "1699999999999-1"
   2) 1) "customer"
      2) "43"

> XREVRANGE orders + - COUNT 1      # newest entry first
> XRANGE orders 1699999000000 1699999999999   # a time window (IDs embed ms)

go deeper

for a junior

Be able to say XRANGE reads a slice of existing history and XREAD returns entries newer than an ID you supply, optionally blocking for new ones.

for a middle

Add the inclusive-versus-exclusive boundary difference, -/+/$/0 semantics, COUNT, XREVRANGE for the newest N, and that neither command deletes.

for a senior

Discuss the practical pattern — bounded XRANGE paging to catch up, then XREAD BLOCK to tail — plus the at-least-once versus at-most-once consequence of when you persist the last ID.

for a principal

Frame plain XREAD as a client-tracked cursor with no server-side position or acknowledgement, and be explicit about what that means for delivery guarantees and for memory, since nothing is reclaimed by reading.

## Two different questions The distinction is not "old versus new data" but **which question you are asking**: - `XRANGE` asks *"what is in this stream between these two points?"* — a query over a stored, ordered collection. - `XREAD` asks *"what has appeared since I was last here?"* — a cursor advance, optionally waiting. ## XRANGE in detail ``` XRANGE key start end [COUNT count] ``` - `start` and `end` are entry IDs. `-` is the smallest possible ID and `+` the largest, so `XRANGE s - +` returns everything. - **Both ends are inclusive.** Prefixing an ID with `(` makes it exclusive (Redis 6.2+): `XRANGE s (1526-0 +`. - A partial ID is completed for you: as a start, `1526919030474` means `...-0`; as an end it means `...-<max seq>`. That makes "everything in this millisecond" a one-liner, and range-by-time straightforward since IDs embed the timestamp. - `COUNT` bounds the reply size. Without it, a wide range on a large stream can return an enormous reply and block the server while it serializes — the main way people hurt themselves with XRANGE. - Complexity is O(log N) to locate the start plus O(M) for the M entries returned. `XREVRANGE key end start [COUNT n]` swaps the argument order (high boundary first). `XREVRANGE s + - COUNT 10` is the idiomatic "last 10 entries". ## XREAD in detail ``` XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...] ``` - The IDs you pass are **exclusive**: you get entries with a strictly greater ID. This is exactly what you want when the ID you pass is "the last one I handled". - Passing `0` (or `0-0`) means "everything from the beginning", which is how you resume a cold consumer. - Passing `$` means "the current last ID" — only entries added **after** this call. `$` is resolved at call time, so it is only meaningful for the first call; afterwards you pass the last ID you actually received. - Multiple streams can be read in one call: all keys first, then all IDs, in matching order. The reply groups entries per stream. - `BLOCK ms` makes the call wait up to that long for new data (`BLOCK 0` = wait forever) and return nil on timeout. Without `BLOCK` the call is a non-blocking poll that may return nil immediately. ## Neither one consumes A common misconception carried over from queues: reading does **not** remove entries. `XRANGE` and `XREAD` are pure reads. The stream keeps growing until you trim it (`XTRIM`, or the trim options on `XADD`) or delete entries (`XDEL`). That is deliberate — it is what allows many independent readers and replay from any point — but it means memory management is an explicit decision, not a side effect of consuming. Similarly, plain `XREAD` keeps **no server-side position**. If your process crashes after reading and before storing the last ID, you will re-read those entries; if you store the ID before processing, you may skip them. Load-balanced fan-out with server-tracked positions and acknowledgement is a different mechanism entirely and not what plain `XREAD` provides. ## Choosing, in practice Use **XRANGE / XREVRANGE** when: - Rendering "the last 20 events" in a UI (`XREVRANGE s + - COUNT 20`). - Replaying a time window because IDs carry the millisecond (`XRANGE s 1699999000000 1699999999999`). - Paging through history for an export or a backfill. - Inspecting a stream by hand in `redis-cli`. Use **XREAD** when: - A worker should follow the stream continuously, starting from `$` (new only) or a stored last ID (resume). - You want to watch several streams from one connection. - You want the server to wake you on arrival rather than polling in a loop. A very common combination is: on startup, `XRANGE` (or `XREAD` with `0`) to catch up on the backlog quickly in bounded pages, then switch to `XREAD ... BLOCK` from the last ID you processed for the live tail. ## Small gotchas - Both return entries as `[id, [field, value, field, value, ...]]`; the field list is flat, not a map, in RESP2. - `XRANGE` on a nonexistent key returns an empty array, not an error; `XREAD` returns nil. - Because XRANGE's boundaries are inclusive and XREAD's are exclusive, naively swapping one for the other in paging code duplicates or drops an entry — the classic off-by-one in stream code.

  • Does reading a stream with XREAD delete the entries it returns?
    No. Both XREAD and XRANGE are pure reads; entries stay in the key until you trim the stream with XTRIM or the trim options on XADD, or remove specific entries with XDEL. This is what lets several independent readers consume the same stream and lets you replay history, but it also means memory growth must be managed deliberately.
  • What is the difference between passing `0` and `$` as the ID to XREAD?
    `0` means "return everything with an ID greater than 0-0", i.e. the whole stream from the beginning — used when a consumer starts cold or resumes without a stored cursor. `$` resolves to the stream's current last ID at the moment of the call, so you only receive entries added afterwards. `$` is only meaningful for the first call; subsequent calls must pass the last ID actually received, or entries arriving between calls are missed.
  • How would you fetch the ten most recent entries?
    `XREVRANGE key + - COUNT 10` — XREVRANGE takes the high boundary first and walks backwards, so with `+` and `-` and a COUNT you get the newest ten in descending order. Doing it with XRANGE would require knowing the starting ID in advance or reading the whole stream, which is why XREVRANGE exists.

saying these in an interview costs you the question

  • Believing XREAD removes or consumes entries like a queue pop.
  • Forgetting that XRANGE boundaries are inclusive while XREAD's ID is exclusive, causing duplicate or skipped entries.
  • Thinking the server remembers a plain XREAD client's position.
  • Calling XRANGE with `- +` on a large stream without COUNT.
  • Passing `$` on every XREAD call in a loop, which silently drops anything that arrived between calls.

context

open as a page

In Redis, what delivery guarantee does a message sent with the PUBLISH command carry, and what happens to messages published while a subscriber's connection is down?

level: middleimportance: must knowfreq 62%

basics

~20 s

Fire-and-forget, at-most-once. PUBLISH hands the message only to clients subscribed at that instant and stores nothing. A subscriber that is disconnected, still reconnecting, or killed for a full output buffer misses those messages permanently: no replay, no offsets, no acknowledgement.

open as a page

When you append to a Redis Stream with XADD and pass * as the ID, what ID does Redis generate, and how is that identifier structured?

level: middleimportance: must knowfreq 55%

basics

~20 s

Redis generates an ID of the form milliseconds-sequence, e.g. 1699999999999-0. The first part is the server's Unix time in milliseconds, the second a counter that increments for additional entries within the same millisecond. IDs are strictly increasing, so XADD rejects any ID not greater than the stream's last one.

open as a page

Several worker processes need to share the entries of one Redis Stream so that each entry is initially handed to only one worker. How do you set that up with XGROUP CREATE and XREADGROUP, and what does the special ID `>` mean in XREADGROUP?

level: middleimportance: must knowfreq 55%

basics

~20 s

Create the group once: XGROUP CREATE key group $ (use 0 to replay history, MKSTREAM if the stream may not exist). Each worker reads with XREADGROUP GROUP group consumer-name STREAMS key >. The > means entries never yet delivered to anyone in this group, so Redis gives each new entry to one consumer only.

open as a page

After a worker receives an entry from a Redis Stream via XREADGROUP, what state does the server keep about that entry until XACK is called, and how do you inspect it?

level: middleimportance: must knowfreq 50%

basics

~20 s

The entry goes into the group's Pending Entries List (PEL): entry ID, owning consumer, last delivery time and delivery count. XACK key group id removes it from the PEL. Inspect with XPENDING (summary) or XPENDING key group IDLE ms start end count (per-entry detail).

open as a page

For an at-least-once job queue where every job must eventually be processed, why is Redis Pub/Sub the wrong primitive, and what do Redis Streams provide instead?

level: middleimportance: must knowfreq 50%

basics

~20 s

Pub/Sub keeps nothing: it broadcasts once and forgets, so a worker that is restarting or crashes mid-job loses that job. A Redis Streams consumer group retains each entry, gives one consumer ownership of it, and keeps it pending and recoverable until XACK.

open as a page

How does XREAD with the BLOCK option behave when tailing a Redis Stream, and what does passing the special ID $ mean?

level: seniorimportance: must knowfreq 45%

basics

~20 s

BLOCK ms parks the connection until an entry newer than the given ID arrives or the timeout expires (BLOCK 0 waits forever); on timeout it returns nil. $ resolves to the stream's current last ID at call time, so you get only entries added afterwards — safe for the first call only, then you must pass the last ID you actually received.

open as a page

A worker consuming a Redis Stream with XREADGROUP crashed while holding entries it had not acknowledged. What happens to those entries, and how do you get them processed by a surviving worker?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Nothing happens automatically: they stay in the group's pending list owned by the dead consumer's name forever. Another consumer must take ownership — XAUTOCLAIM key group new-consumer min-idle-time 0 (or XCLAIM on specific IDs found via XPENDING IDLE). The min-idle-time guard prevents stealing from a merely slow worker.

open as a page

Three independent services must each see every event, and each service runs four replicas that should share its own workload. Compare how Redis Pub/Sub and Redis Streams handle that fan-out shape.

level: seniorimportance: must knowfreq 48%

basics

~20 s

Pub/Sub broadcasts to every subscriber, so all twelve replicas process every event - it cannot load-balance within a service. Redis Streams give both shapes: create one consumer group per service (each group sees all entries) and run the four replicas as consumers inside that group so entries are split among them.

open as a page

In Redis Pub/Sub, what does the PSUBSCRIBE command do that SUBSCRIBE does not, and what exactly does a subscribed client receive when a published message matches?

level: juniorimportance: should knowfreq 38%

basics

~20 s

PSUBSCRIBE registers a glob pattern (* ? [ab]) instead of an exact channel name. When a publish matches, the client gets a pmessage push with four parts: the literal 'pmessage', the pattern that matched, the actual channel published to, and the payload.

open as a page

After a Redis client issues the SUBSCRIBE command, what state is that connection in, which commands can it still run, and how does it get back to normal?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Under RESP2 the connection enters subscriber mode: it may only run SUBSCRIBE/UNSUBSCRIBE (and their pattern and sharded variants), plus PING, RESET and QUIT. Other commands error. Leave by unsubscribing from everything, or with RESET. Under RESP3 the restriction is gone.

open as a page

A Redis client issues SUBSCRIBE news.sports and also PSUBSCRIBE news.* on the same connection, then a publisher runs PUBLISH news.sports hello. How many messages does that client receive, and why?

level: middleimportance: should knowfreq 30%

basics

~20 s

Two: one message push from the exact subscription and one pmessage push from the pattern. Redis delivers once per matching subscription and never de-duplicates per client, so overlapping subscriptions mean duplicate payloads the application must handle.

open as a page

How do you page through a large Redis Stream with XRANGE without re-reading or skipping entries at the page boundaries?

level: middleimportance: should knowfreq 35%

basics

~20 s

Ask for COUNT+ entries from a start ID, then make the next page start just after the last ID you got. Since XRANGE boundaries are inclusive, either use the exclusive prefix — XRANGE key (lastId + COUNT n — or increment the sequence part of that ID yourself.

open as a page

How does the cost of a Redis PUBLISH grow as the number of registered pattern subscriptions increases, and why can heavy PSUBSCRIBE usage add latency for clients doing unrelated work on the same instance?

level: seniorimportance: should knowfreq 28%

basics

~20 s

PUBLISH is roughly O(N+M): N subscribers on the exact channel (a hash lookup plus a write each) and M registered patterns, every one glob-matched against the channel name on every publish. That matching runs on the same single command loop everyone else waits on.

open as a page

In Redis Cluster, how does a message sent with PUBLISH reach subscribers on other nodes, why does that become a scaling problem, and what do SPUBLISH and SSUBSCRIBE do differently?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Classic PUBLISH is broadcast to every node over the cluster bus so any subscriber anywhere receives it, which makes pub/sub traffic grow with cluster size and never shard. Sharded pub/sub (Redis 7.0) hashes the channel name to a slot: SPUBLISH goes only to that shard's primary and replicas, and SSUBSCRIBE must target that node.

open as a page

A Redis subscriber consumes published messages more slowly than they are produced. What does the Redis server eventually do about it, which configuration governs that, and what does the application see?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Redis buffers undelivered messages per client, and when the pub/sub client-output-buffer-limit is crossed (default hard 32mb, soft 8mb for 60s) it closes that connection. The subscriber sees a dropped connection, reconnects, resubscribes, and silently loses everything queued. Publishers see nothing.

open as a page

A Redis Stream grows forever unless you cap it. How do XTRIM and the MAXLEN option on XADD work, and what does the tilde in MAXLEN ~ 1000 actually change?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Nothing is removed by reading, so you trim explicitly: XTRIM key MAXLEN n (or MINID id), or the same options inline on XADD. The tilde makes trimming approximate — Redis removes whole internal macro-nodes and may leave a few extra entries, which is O(1)-ish instead of proportional to the entries removed.

open as a page

A team proposes replacing a durable partitioned message log with Redis Streams. What does Redis Streams give up compared with a disk-backed replicated log, and where is it genuinely the better choice?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Redis Streams keep history in RAM on one shard, replicate asynchronously, and trim by length or ID - so retention is short, an acknowledged write can be lost on failover, and one stream key cannot be partitioned across nodes. They win on latency, operational simplicity, and when Redis is already in the stack.

open as a page

You are designing a job pipeline on Redis Streams with consumer groups. What delivery guarantee does the XREADGROUP/XACK cycle actually give, and how would you design the handlers, retry policy and failure handling around it?

level: principalimportance: should knowfreq 38%

basics

~20 s

At-least-once: an entry is acknowledged only after the work runs, so a crash between the two causes redelivery. Design idempotent handlers keyed on the entry ID, claim stale entries with a min-idle-time above p99 job time, cap retries via the delivery counter, and route poison entries to your own dead-letter stream.

open as a page

Your platform team owns the guidance for new event flows at a company where every team already runs its own Redis instance. Take the technical comparison as given — Redis Pub/Sub has no acknowledgement and drops messages for disconnected or slow subscribers, Redis Streams keep a bounded in-memory history on a single key with consumer groups, and a partitioned durable log keeps days of replay with per-key ordering. What organizational rule decides whether a flow is allowed to stay on Redis at all, what do you require of every flow regardless of which primitive is chosen, and what do you tell a team whose acknowledged write must never be lost?

level: principalimportance: should knowfreq 28%

basics

~20 s

Ask who is on the other end of the contract and how long it lives. Team-internal, short-lived, team-operated flows stay in Redis; cross-team, long-lived, unenumerable consumers move to a durable log. Always require idempotent consumers, sized retention with lag alerting, a written durability expectation, and a dead-letter path.

open as a page

How do you cancel a Redis pattern subscription created with PSUBSCRIBE, and what happens if you instead call UNSUBSCRIBE with a channel name that the pattern matched?

level: middleimportance: nice to knowfreq 22%

basics

~10 s

Use PUNSUBSCRIBE with the exact pattern string, byte-for-byte. UNSUBSCRIBE only removes exact-channel subscriptions and never touches patterns, so the pmessage deliveries keep arriving. PUNSUBSCRIBE with no arguments removes every pattern on the connection.

open as a page