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?
answer
- PEL record = id + consumer + last-delivery-time + delivery count
- XACK removes from PEL, never from the stream
- XACK returns 0 = it was not pending
- XPENDING summary vs extended (IDLE, start, end, count)
- work first, ack after; NOACK = fire and forget
basics
~20 sThe 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).
solid answer
~50 sEvery entry served by `XREADGROUP ... >` is recorded in the group's Pending Entries List. Each PEL record holds the entry ID, the consumer that owns it, the timestamp of its last delivery and a delivery counter. It is per group, with a per-consumer view inside it. `XACK mystream mygroup 1712-0` removes the record and returns how many IDs were actually removed (0 means it was not pending — already acked, or claimed by someone else). Acknowledging does **not** delete the entry from the stream; the stream is trimmed separately with MAXLEN/MINID. Inspection has two forms. The summary form, `XPENDING mystream mygroup`, gives the total pending count, the min and max pending IDs, and a per-consumer count — cheap enough to poll for monitoring. The extended form, `XPENDING mystream mygroup IDLE 60000 - + 10 [consumer]`, lists individual entries with owner, idle milliseconds and delivery count, which is what you use to find stuck or repeatedly-failing entries.
code
text · 10 lines# how far behind is the group, and who holds what?
XPENDING orders billing
# 1) 42 2) 1712-0 3) 1898-0 4) 1) 1) "worker-1" 2) "40"
# 2) 1) "worker-3" 2) "2"
# which entries have been untouched for over a minute?
XPENDING orders billing IDLE 60000 - + 10
# finish the job, then acknowledge
XACK orders billing 1712345678901-0 # => 1 (0 means it was not pending)go deeper
Know that a delivered entry sits in the pending list until XACK, and that acking does not delete it from the stream.
Describe the four fields of a PEL record, both XPENDING forms, and the read-work-ack ordering with its at-least-once consequence.
Use XPENDING IDLE and delivery counts as diagnostics, watch pending totals and stream length as separate signals, and explain the interaction between trimming and still-pending entries.
Treat pending count and lag as the control signals for the pipeline — capacity, alerting thresholds, retention bounds chosen against the slowest group, and the memory cost of a PEL that stops shrinking.
## The window between delivery and acknowledgement When a consumer reads new entries with `XREADGROUP GROUP g c STREAMS s >`, Redis does not just hand them over. For each entry it writes a record into the group's Pending Entries List — the PEL. The PEL is the reason consumer groups can survive a dead worker at all: it is the server's memory of "this entry was given out and nobody has said they finished it". Each PEL record contains four things: - the entry ID, - the name of the consumer currently owning it, - the timestamp of the last time it was delivered (from which idle time is derived), and - a delivery counter, incremented each time the entry is handed out again. The PEL is per group. Conceptually there is one global PEL for the group and a per-consumer subset of it; XPENDING and XINFO let you look at either. ## XACK: what it does and does not do ``` XACK mystream mygroup 1712345678901-0 ``` XACK removes the record from the PEL and returns the number of IDs it actually removed. A return of 0 is informative: the entry was never pending, was already acknowledged, or has since been claimed by another consumer, so your acknowledgement matched nothing. Silently ignoring that return value is how double-processing goes unnoticed. Two things XACK does **not** do, and both are common misconceptions: 1. **It does not delete the entry from the stream.** Stream length is independent of acknowledgement. Entries are removed by `XDEL`, or by trimming with `XADD ... MAXLEN ~ 100000` / `XTRIM mystream MINID <id>`. A stream whose consumers ack diligently but which is never trimmed grows without bound, and that is one of the most common Redis memory surprises. 2. **It does not free the consumer.** Consumer objects persist in the group until `XGROUP DELCONSUMER` removes them. The converse hazard also exists: trimming can remove a stream entry that is still pending. The PEL record then points at an entry that no longer exists. Since Redis 7.0, XAUTOCLAIM cleans those records up and reports them separately; before that they had to be acknowledged manually. ## Ordering: when to acknowledge The correct sequence is read → do the work → XACK. Acknowledging first (or using the `NOACK` flag on XREADGROUP, which skips the PEL entirely) turns delivery into fire-and-forget: if the worker dies mid-job nobody can tell, and the entry is lost from the group's point of view. `NOACK` is a legitimate choice for cheap, lossy telemetry, but it must be a deliberate one. Because acknowledgement happens after the side effect, the reverse failure is possible too: the work succeeds, the process dies before XACK, and the entry is redelivered later. That is the at-least-once shape of the mechanism, and it is why handlers need to be idempotent. ## Inspecting the PEL The **summary form** is one command with no range: ``` XPENDING mystream mygroup ``` It returns the number of pending entries, the smallest and largest pending IDs, and a list of consumer/count pairs. It is O(1)-ish with respect to the PEL size for the counts, which makes it suitable as a monitoring probe: a pending count that grows monotonically means work is being handed out faster than it is finished, or that some consumer is stuck. The **extended form** takes a range and count, and optionally an idle filter and a consumer name: ``` XPENDING mystream mygroup IDLE 60000 - + 10 XPENDING mystream mygroup - + 10 worker-3 ``` Each row gives entry ID, owning consumer, idle time in milliseconds, and delivery count. `IDLE 60000` filters to entries untouched for over a minute — the practical query for "what has a dead or wedged worker got its hands on". A high delivery count on one entry is the signature of a poison message: it is being redelivered and failing repeatedly. `XINFO GROUPS mystream` complements this with the group-level pending total, the last delivered ID and, since 7.0, `lag` (how many entries are still undelivered). `XINFO CONSUMERS mystream mygroup` gives per-consumer pending counts and idle times, which distinguishes "a consumer is stuck" from "the whole group is behind". ## Reading your own pending entries back A restarting worker should first drain what it already owns before asking for new work: ``` XREADGROUP GROUP mygroup worker-3 COUNT 10 STREAMS mystream 0 ``` An explicit ID (rather than `>`) returns this consumer's pending entries with a greater ID, without blocking and without touching the group cursor. When that comes back empty, switch to `>`. Entries owned by a consumer name that never comes back are not covered by this and need claiming instead. ## Memory shape The PEL costs memory proportional to the number of unacknowledged entries. A group whose workers stopped acking keeps growing that structure even if the stream itself is trimmed, so both the stream length and the pending count deserve monitoring.
- Your consumers acknowledge everything correctly, yet the stream keeps growing and Redis memory climbs. Why?XACK only clears the PEL; it never removes entries from the stream itself. Unless you trim, every entry ever written is retained. Add a capped write (`XADD mystream MAXLEN ~ 1000000 * ...`) or a periodic `XTRIM mystream MINID <cutoff>`, choosing the bound so that it stays comfortably ahead of the slowest group's pending range.
- XACK returns 0 for an entry you just processed. What are the likely explanations?The record is no longer in the PEL: either it was already acknowledged (a duplicate delivery you processed twice), or another consumer claimed it while you were slow and then acked it, or you are acking against the wrong group or key. Treat a 0 as a signal that this unit of work may have been done elsewhere, and rely on an idempotent handler rather than on the ack succeeding.
saying these in an interview costs you the question
- Thinking XACK deletes the entry from the stream, so trimming is never configured
- Acknowledging before doing the work, or reaching for NOACK to "simplify", which loses failed entries silently
- Assuming the PEL is per consumer only, and missing that the group owns it and entries can change owner
- Ignoring the XACK return value
- Believing an unacknowledged entry is redelivered automatically after a timeout — something must claim it