skip to content

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