Engineer reviewing Redis Streams pending entries, consumer ownership, and work completion evidence.

A Redis Stream Message Was Claimed. Was Its Work Actually Completed?

Wed, Nov 11, 2026

A successful XAUTOCLAIM reply proves a Redis state transition: an eligible pending entry was found and ownership was transferred to another consumer. It does not prove that the application work represented by that entry finished. Redis documents those concepts separately. XAUTOCLAIM transfers ownership of eligible pending messages; XACK removes a message from the consumer group's Pending Entries List after successful processing. The gap between those operations is where duplicate processing, premature acknowledgment, stale workers, and unreconciled external effects become release problems.

This playbook treats that gap as an evidence problem. The scope is deliberately narrow: Redis Streams consumer-group pending entries, XAUTOCLAIM ownership transfer, idle thresholds, duplicate processing, deleted entries in a PEL, and XACK reconciliation. It does not generalize into a Redis tutorial, queue-product comparison, or production migration guide.

The fixture below is disposable, synthetic, and pinned to Redis Open Source 7.2.17 on linux/amd64. The official Redis release index lists 7.2.17 as released on October 26, 2026; Redis's published hash repository records SHA-256 07c679c013fd82513299733bddf91999bbbc830351ec9ea985a533b09a595d76 for the 7.2.17 source tarball. Docker Hub currently publishes an amd64 7.2.17 image manifest digest of sha256:b690ae83413c6051e087c9fb92a420b002e820a9fe434ef6e266d612e3915306.

I did not execute the Redis fixture in the research environment used to prepare this article. Accordingly, every runtime outcome below is explicitly labeled EXPECTED / NOT EXECUTED. The final validation decision is therefore RERUN, not ACCEPT.

Define the admission and release contract

The admission contract is stricter than “Redis returned the message.” A case enters the recovery procedure only when the evidence establishes four identities: stream, consumer group, entry ID, and application work identity. The work identity must be stable across redelivery; in this fixture it is the synthetic job_id field. Without that identity, two deliveries cannot be reconciled into “same work attempted twice” versus “two legitimate jobs.”

Redis creates pending entries when messages are delivered through consumer groups and remain unacknowledged. XPENDING is the inspection interface for that PEL, including current owner, idle time, and delivery count in its detailed form. XACK, separately, removes acknowledged IDs from the PEL.

The release contract is:

A recovered entry is accepted only when the expected entry identity, current Redis ownership, independently recorded work effect, acknowledgment result, and final PEL state reconcile without missing or contradictory records.

That means a successful XAUTOCLAIM, a zero shell exit status, and even a successful XACK are insufficient individually. XACK accepts a stream, group, and ID; there is no consumer argument in the command syntax. Redis therefore records that the group's pending reference was acknowledged, not which application process actually completed an external side effect.

Refonte's guide to verifying committed rows in SQLite WAL backups likewise separates a technically successful operation from independently reconciled application state. Its technical subject, fixture, and recovery decision are SQLite-specific, so this Redis procedure should not borrow its WAL workflow.

For this Redis playbook, ACCEPT means the required work identity exists exactly as expected, duplicate attempts are accounted for, the intended acknowledgment happened, and the final PEL reconciles. REPAIR means the defect is understood and locally correctable. RERUN means the test or evidence set is incomplete but can safely be reproduced. HOLD means the operator cannot determine which external work has already occurred, making automated replay unsafe.

Pin the baseline and source versions

Use a version that contains the behavior under examination without silently depending on newer Streams semantics. XAUTOCLAIM has existed since Redis 6.2; deletion of missing stream entries from the PEL during XAUTOCLAIM was added in Redis 7.0. Redis's current command reference also contains later capabilities such as XREADGROUP ... CLAIM introduced in 8.4, so those newer options must not leak into a 7.2 validation fixture.

Redis's 7.2 command index confirms XAUTOCLAIM, XPENDING, XREADGROUP, and XACK are in the 7.2 command set. The generic “latest” pages currently expose newer syntax alongside historical information, which is why the fixture pins a runtime rather than assuming that today's latest documentation syntax equals the 7.2 runtime contract.

The baseline is:

Component

Pinned identity

Verification

Redis server/client

Redis Open Source 7.2.17

redis-server --version, redis-cli --version

Platform

linux/amd64

docker inspect

Container manifest

sha256:b690ae83413c6051e087c9fb92a420b002e820a9fe434ef6e266d612e3915306

Docker image inspection

Source tarball

redis-7.2.17.tar.gz

Official release archive

Source SHA-256

07c679c013fd82513299733bddf91999bbbc830351ec9ea985a533b09a595d76

Official Redis hashes repository

The Redis archive and hash repository supply independently checkable source identity, while Docker Hub supplies the image manifest identity used by the runnable fixture.

Pin the runtime and source hashes

Assertion: the runtime used for evidence collection must identify itself as Redis 7.2.17, and its container digest and source-tarball hash must match the frozen baseline.

Expected outcome: all three identities match exactly.

Rejection condition: any floating image tag, different architecture, different Redis version, or hash mismatch makes behavioral evidence non-comparable until the baseline is updated deliberately.

export REDIS_IMAGE='redis:7.2.17@sha256:b690ae83413c6051e087c9fb92a420b002e820a9fe434ef6e266d612e3915306'
export REDIS_SOURCE_SHA256='07c679c013fd82513299733bddf91999bbbc830351ec9ea985a533b09a595d76'
docker pull --platform linux/amd64 "$REDIS_IMAGE"
docker image inspect "$REDIS_IMAGE" \
  --format '{{json .RepoDigests}}'
docker run --rm --platform linux/amd64 "$REDIS_IMAGE" \
  redis-server --version
docker run --rm --platform linux/amd64 "$REDIS_IMAGE" \
  redis-cli --version

Runtime result: EXPECTED / NOT EXECUTED. Expected server identity is Redis 7.2.17. No observed output is claimed here.

Build an isolated fixture with synthetic inputs

Create one disposable container, bind it only to loopback, disable persistence, and keep every key under a laboratory prefix. Persistence is unnecessary because the acceptance object is the captured evidence ledger, not survival across Redis restarts.

set -eu
export NAME='redis-xautoclaim-lab'
export PORT='6389'
export R='docker exec redis-xautoclaim-lab redis-cli --json -p 6379'
export LAB="$PWD/redis-xautoclaim-lab"
rm -rf "$LAB"
mkdir -p "$LAB/effects" "$LAB/evidence"
docker rm -f "$NAME" >/dev/null 2>&1 || true
docker run -d \
  --name "$NAME" \
  --platform linux/amd64 \
  -p "127.0.0.1:${PORT}:6379" \
  "$REDIS_IMAGE" \
  redis-server --save '' --appendonly no
docker exec "$NAME" redis-cli PING
docker exec "$NAME" redis-server --version > "$LAB/evidence/runtime.txt"
docker inspect "$NAME" > "$LAB/evidence/container-inspect.json"

Each test stream uses explicit stream IDs rather than *, which prevents generated timestamps from becoming case identities. Each message contains a job_id that names the synthetic application operation. The external effect is represented by an atomic directory creation on the host:

apply_effect() {
  job_id="$1"
  consumer="$2"

  printf '%s\t%s\n' "$job_id" "$consumer" \
    >> "$LAB/evidence/attempts.tsv"

  if mkdir "$LAB/effects/$job_id" 2>/dev/null; then
    printf '%s\t%s\tCOMPLETED\n' "$job_id" "$consumer" \
      >> "$LAB/evidence/completions.tsv"
    return 0
  else
    printf '%s\t%s\tDUPLICATE_BLOCKED\n' "$job_id" "$consumer" \
      >> "$LAB/evidence/duplicates.tsv"
    return 10
  fi
}

This filesystem marker is not presented as a production transaction design. It is merely independent synthetic evidence that is not derived from Redis's PEL. That independence matters: if both “expected work” and “observed work” were inferred from the same Redis response, the reconciliation would be circular.

The SQLite BUSY_SNAPSHOT transaction retry article also uses an intentionally constrained laboratory to find the correct retry boundary, but its snapshot and transaction semantics are unrelated to consumer-group ownership. Here, the shared validation method is to reproduce the ambiguous success condition before approving the repair.

Author the independent expected-case manifest

Freeze the oracle before issuing the first XREADGROUP. The manifest should say what each case is intended to establish, not copy whatever Redis later reports.

Case

Required condition

Expected Redis state

Expected work state

Status before run

C01

Original consumer reads, no ACK

One PEL entry owned by c1

No completion

EXPECTED / NOT EXECUTED

C02

Idle threshold not reached

Entry remains with c1

No completion

EXPECTED / NOT EXECUTED

C03

Second consumer claims after threshold

PEL owner becomes c2

Still no completion

EXPECTED / NOT EXECUTED

C04

Original consumer resumes after transfer

Redis does not restore ownership to c1 merely because it resumes

Duplicate external attempt remains possible from stale in-process payload

EXPECTED / NOT EXECUTED

C05

Repeated claim after completed-but-unacked work

Ownership can move again after new idle interval

Idempotent effect guard blocks second completion

EXPECTED / NOT EXECUTED

C06

Stream entry deleted while pending

XAUTOCLAIM reports deleted ID and removes stale PEL reference

No payload available for recovery

EXPECTED / NOT EXECUTED

C07

New owner ACKs recovered entry

ID removed from PEL

Exactly one completion

EXPECTED / NOT EXECUTED

C08

Cleanup reconciliation

Pending count is zero for completed fixture cases

Expected completion set equals observed completion set

EXPECTED / NOT EXECUTED

These expectations follow the documented contract. XAUTOCLAIM only claims entries meeting the minimum idle threshold, resets idle time when claiming, and increments attempted-delivery count unless JUSTID is used. Redis also documents that deleted stream entries encountered by XAUTOCLAIM are removed from the PEL and returned in the deleted-ID portion of the reply, a behavior present since 7.0.

Freeze the oracle before executing

Assertion: manifest.tsv is written and hashed before any case command that changes consumer-group state.

Expected outcome: the manifest hash predates all case evidence and remains unchanged.

Rejection condition: regenerating expected rows from actual Redis replies invalidates independence.

cat > "$LAB/manifest.tsv" <<'EOF'
C01 job-A   c1_read_no_ack  pending_c1  no_completion
C02 job-A   pre_idle_claim_rejected pending_c1  no_completion
C03 job-A   c2_claims   pending_c2  no_completion
C04 job-A   c1_resumes  owner_remains_c2    duplicate_risk
C05 job-B   reclaim_after_effect    pending_new_owner   one_completion
C06 job-C   delete_while_pending    pel_reference_removed   no_recovery_payload
C07 job-D   new_owner_ack   pending_zero    one_completion
C08 all final_reconcile pending_zero    expected_equals_observed
EOF

sha256sum "$LAB/manifest.tsv" \
  > "$LAB/evidence/manifest.sha256"

Manifest creation: EXPECTED / NOT EXECUTED.

Reproduce the misleading success condition

Case C01 establishes a normal pending entry. Add one message, create the group from 0, let c1 receive it with XREADGROUP, and deliberately omit XACK.

docker exec "$NAME" redis-cli DEL lab:c1 >/dev/null
docker exec "$NAME" redis-cli XADD lab:c1 1000-0 job_id job-A payload synthetic
docker exec "$NAME" redis-cli XGROUP CREATE lab:c1 g 0
docker exec "$NAME" redis-cli --json \
  XREADGROUP GROUP g c1 COUNT 1 STREAMS lab:c1 '>' \
  | tee "$LAB/evidence/C01-read.json"
docker exec "$NAME" redis-cli --json \
  XPENDING lab:c1 g - + 10 \
  | tee "$LAB/evidence/C01-pending.json"

C01 expected / not executed: 1000-0 is pending for c1; there is no effects/job-A directory.

For C02, immediately attempt a claim with a threshold that cannot yet have elapsed:

docker exec "$NAME" redis-cli --json \
  XAUTOCLAIM lab:c1 g c2 60000 0-0 COUNT 10 \
  | tee "$LAB/evidence/C02-too-young.json"

C02 expected / not executed: no entry is claimed. XPENDING ... IDLE 60000 would likewise exclude the young entry because Redis's detailed XPENDING supports an idle-time filter in milliseconds.

Now demonstrate the dangerous interpretation. Wait long enough for a smaller threshold, then claim:

sleep 1.2
docker exec "$NAME" redis-cli --json \
  XAUTOCLAIM lab:c1 g c2 1000 0-0 COUNT 10 \
  | tee "$LAB/evidence/C03-claim.json"
docker exec "$NAME" redis-cli --json \
  XPENDING lab:c1 g - + 10 \
  | tee "$LAB/evidence/C03-pending.json"
test ! -e "$LAB/effects/job-A"

C03 expected / not executed: the PEL owner becomes c2, XAUTOCLAIM returns the message, and the independent completion marker is still absent.

That is the false-positive success condition: claim succeeded, work did not. Redis describes XAUTOCLAIM as changing ownership, not as executing application logic.

Run the controlled positive comparison

A positive control must differ by one meaningful step: the recovered consumer actually records the synthetic work completion before acknowledging.

docker exec "$NAME" redis-cli DEL lab:positive >/dev/null
docker exec "$NAME" redis-cli XADD lab:positive 2000-0 job_id job-D payload synthetic
docker exec "$NAME" redis-cli XGROUP CREATE lab:positive g 0
docker exec "$NAME" redis-cli --json \
  XREADGROUP GROUP g c1 COUNT 1 STREAMS lab:positive '>' \
  > "$LAB/evidence/C07-initial-read.json"
sleep 1.2
docker exec "$NAME" redis-cli --json \
  XAUTOCLAIM lab:positive g c2 1000 0-0 COUNT 10 \
  > "$LAB/evidence/C07-claim.json"
apply_effect job-D c2 || exit 1
docker exec "$NAME" redis-cli --json \
  XACK lab:positive g 2000-0 \
  | tee "$LAB/evidence/C07-xack.json"
docker exec "$NAME" redis-cli --json \
  XPENDING lab:positive g \
  | tee "$LAB/evidence/C07-pending-summary.json"

Redis documents the intended order explicitly: once a consumer successfully processes a message, it should call XACK; the acknowledgment removes the PEL entry. The integer returned by XACK is the number of IDs that were actually acknowledged, and IDs already absent from the PEL are not counted.

C07 expected / not executed: apply_effect records one completion; XACK returns 1; XPENDING summary reports zero pending messages.

This positive control is comparable with the negative case because both involve a pending entry, an idle interval, and ownership transfer. The decisive difference is independent evidence of completion before acknowledgment.

That evidence-first structure is also why GitHub Actions matrix coverage is a relevant comparison: its release denominator is independently authored rather than inferred from a green aggregate job. The technical identities here are Redis entry IDs and work IDs, not workflow matrix cases.

Retain the negative-control evidence

Assertion: the C03 claim evidence and absence of effects/job-A must remain in the final evidence bundle.

Expected outcome: the fixture demonstrates that Redis ownership transfer can be true while application completion is false.

Rejection condition: deleting the negative control and retaining only the successful process-and-ACK path weakens the test into a happy-path demonstration.

The negative control is not a failure of Redis. It is evidence that the operational statement “message claimed” is narrower than “work completed.”

Probe failures and boundary conditions

The first boundary is original-consumer resumption. Redis documents that XREADGROUP with an ID other than > accesses pending entries belonging to the requesting consumer. After ownership is transferred to c2, Redis should not hand that PEL entry back to c1 merely because c1 asks for its pending history.

docker exec "$NAME" redis-cli --json \
  XREADGROUP GROUP g c1 STREAMS lab:c1 0 \
  | tee "$LAB/evidence/C04-c1-history.json"

C04 expected / not executed: c1 does not regain the transferred PEL entry.

That does not eliminate duplicate-processing risk. The original process may already hold the message payload in memory and may resume application code after a pause, partition, GC stall, scheduler delay, or operator intervention. XAUTOCLAIM changes Redis ownership; it cannot revoke bytes already delivered to another process. This is an inference from the documented delivery and ownership model, not a separate Redis guarantee.

The second boundary is repeated recovery after work completed but before acknowledgment. Use job-B: c2 completes the effect, crashes or stops before XACK, then c3 claims after the entry becomes idle again.

docker exec "$NAME" redis-cli DEL lab:repeat >/dev/null
docker exec "$NAME" redis-cli XADD lab:repeat 3000-0 job_id job-B payload synthetic
docker exec "$NAME" redis-cli XGROUP CREATE lab:repeat g 0
docker exec "$NAME" redis-cli \
  XREADGROUP GROUP g c1 COUNT 1 STREAMS lab:repeat '>' >/dev/null
sleep 1.2
docker exec "$NAME" redis-cli \
  XAUTOCLAIM lab:repeat g c2 1000 0-0 COUNT 10 >/dev/null
apply_effect job-B c2 || exit 1
# Deliberately do not XACK.
sleep 1.2
docker exec "$NAME" redis-cli --json \
  XAUTOCLAIM lab:repeat g c3 1000 0-0 COUNT 10 \
  | tee "$LAB/evidence/C05-reclaim.json"
apply_effect job-B c3 || test "$?" -eq 10
docker exec "$NAME" redis-cli \
  XACK lab:repeat g 3000-0 \
  > "$LAB/evidence/C05-xack.txt"

C05 expected / not executed: the second apply_effect records DUPLICATE_BLOCKED; the completion ledger contains one completion for job-B, while the attempt ledger contains two attempts.

The third boundary is a PEL reference whose stream payload has been deleted.

docker exec "$NAME" redis-cli DEL lab:deleted >/dev/null
docker exec "$NAME" redis-cli XADD lab:deleted 4000-0 job_id job-C payload synthetic
docker exec "$NAME" redis-cli XGROUP CREATE lab:deleted g 0
docker exec "$NAME" redis-cli \
  XREADGROUP GROUP g c1 COUNT 1 STREAMS lab:deleted '>' >/dev/null
docker exec "$NAME" redis-cli XDEL lab:deleted 4000-0
sleep 1.2
docker exec "$NAME" redis-cli --json \
  XAUTOCLAIM lab:deleted g c2 1000 0-0 COUNT 10 \
  | tee "$LAB/evidence/C06-deleted.json"
docker exec "$NAME" redis-cli --json \
  XPENDING lab:deleted g \
  | tee "$LAB/evidence/C06-pending.json"

Redis documents this case directly: from version 7.0, XAUTOCLAIM removes PEL references for entries no longer present in the stream and returns those deleted IDs in the third reply element.

C06 expected / not executed: 4000-0 appears as a deleted ID in the XAUTOCLAIM reply, is not delivered as recoverable work, and no longer remains pending.

Capture a per-case evidence ledger

Do not reduce evidence to one terminal transcript. Preserve a ledger that can answer what was expected, what command changed state, what Redis subsequently reported, and what external-work evidence exists.

Case

Entry

Expected owner/result

Expected completion count

Evidence files

Observed

C01

1000-0

pending c1

0

read, XPENDING

NOT EXECUTED

C02

1000-0

claim rejected by idle threshold

0

XAUTOCLAIM

NOT EXECUTED

C03

1000-0

pending c2

0

XAUTOCLAIM, XPENDING, filesystem absence

NOT EXECUTED

C04

1000-0

no automatic restoration to c1

0

XREADGROUP history

NOT EXECUTED

C05

3000-0

reclaimed by c3, then ACKed

1

XAUTOCLAIM, attempts, duplicates, XACK

NOT EXECUTED

C06

4000-0

stale PEL reference removed

0

XDEL, XAUTOCLAIM, XPENDING

NOT EXECUTED

C07

2000-0

ACKed after completion

1

completion, XACK, XPENDING

NOT EXECUTED

C08

all

zero residual required pending entries

exact manifest

reconciliation output

NOT EXECUTED

The ledger should also retain raw command outputs. Parsed summaries are convenient, but the acceptance review should be able to trace a conclusion back to the original redis-cli --json reply.

Delivery count deserves special treatment. Redis documents that normal XAUTOCLAIM increments attempted-delivery count, while JUSTID does not. That counter is useful recovery evidence, but it is not an application completion counter. A high value can identify repeatedly delivered work; a value of two cannot tell you whether zero, one, or two external side effects occurred.

Reject missing and duplicate records

Assertion: every manifest case has exactly one case-level evidence row, and every expected completion job_id has exactly one completion record.

Expected outcome: no missing case IDs; no duplicate completion identities; duplicate attempts, when deliberately tested, are explicitly classified rather than silently discarded.

Rejection condition: missing evidence, duplicate completion records, or an unclassified extra job moves the decision away from ACCEPT.

A useful shell-level reconciliation is:

sort "$LAB/manifest.tsv" > "$LAB/evidence/manifest.sorted.tsv"
test "$(grep -c '^job-B ' "$LAB/evidence/completions.tsv" 2>/dev/null || true)" -eq 1
test "$(grep -c '^job-D ' "$LAB/evidence/completions.tsv" 2>/dev/null || true)" -eq 1
test "$(grep -c '^job-B ' "$LAB/evidence/attempts.tsv" 2>/dev/null || true)" -eq 2
test "$(grep -c '^job-B ' "$LAB/evidence/duplicates.tsv" 2>/dev/null || true)" -eq 1

Reconciliation result: EXPECTED / NOT EXECUTED.

Reconcile identities and final state

Reconciliation needs two namespaces. Redis identifies a delivery using the stream entry ID, such as 3000-0. The application identifies the logical work using job-B. Do not substitute one for the other.

The stream ID tells you which record Redis delivered and tracks in the PEL. The job_id tells you which external operation must be idempotent. A producer bug could theoretically create two different stream IDs carrying the same business work identity; conversely, one stream message could trigger several downstream writes. This fixture stays simpler by defining one logical work identity per stream entry, but the ledger records both fields so the distinction remains visible.

For each surviving pending entry, capture:

docker exec "$NAME" redis-cli --json XPENDING lab:c1 g - + 100
docker exec "$NAME" redis-cli --json XPENDING lab:repeat g - + 100
docker exec "$NAME" redis-cli --json XPENDING lab:positive g - + 100
docker exec "$NAME" redis-cli --json XPENDING lab:deleted g - + 100

Then compare the final required state. job-B and job-D should have exactly one completion and no residual PEL entry after acknowledgment. job-A is intentionally incomplete in the negative-control stream and therefore must not be represented as accepted work. job-C has no recoverable payload after deletion and must not be invented from its orphaned PEL identity.

This distinction between artifact identity and aggregate “success” resembles the release-pairing discipline in Probability calibrator release validation, where the candidate is accepted only when the right artifact is reconciled with the right model. The Redis-specific identity pair here is stream-entry ID plus logical work ID.

For production adaptation, the independent work ledger should normally come from the authoritative side-effect boundary: a database transaction identifier, an idempotency table, an immutable downstream event ID, or another application-owned completion record. Redis PEL state alone cannot establish that a remote payment, email, data mutation, or API call completed.

Separate documentation from observation

The strongest part of this playbook is the separation between four statement classes.

Documented behavior: Redis states that XAUTOCLAIM transfers ownership of eligible pending entries; only sufficiently idle entries qualify; claiming resets idle time; regular claiming increments attempted deliveries; missing stream entries discovered during the scan are removed from the PEL starting in Redis 7.0.

Documented acknowledgment behavior: Redis states that XACK removes acknowledged IDs from the group's PEL and returns the number actually acknowledged. Redis further says consumers should acknowledge after successful processing.

Hypothesis under test: an operator can mistake successful ownership transfer for successful application work, especially when a recovery worker immediately acknowledges after claiming or when the original worker continues from an already-delivered payload.

Expected runtime result: the fixture should expose that difference through C03 and validate the repaired sequence through C07.

Observed runtime result: NOT EXECUTED. No Redis reply shown in this article is claimed as captured output.

That distinction matters because the current Redis documentation itself has evolved. The latest XREADGROUP page now documents a CLAIM option added in Redis 8.4, whereas this fixture is pinned to 7.2.17 and intentionally uses XAUTOCLAIM.

The related Postgres-versus-Redis queue architecture article discusses whether Redis should remain in an architecture. Refonte's broader articles on real-time streaming analytics and data-engineering tools discuss streaming practices and tools including Kafka. This playbook focuses on the narrower PEL/XAUTOCLAIM/XACK evidence fixture.

Explain the repair boundary

Assertion: the safe repair boundary is the application work unit, not the XAUTOCLAIM command.

Expected outcome: after claiming, the recovery consumer performs or reconciles idempotent work first and acknowledges only when completion evidence satisfies the application's contract.

Rejection condition: any procedure that turns “claimed” directly into “ACK now” without reconciling side effects is incomplete.

Redis supplies delivery recovery primitives. It does not know whether your external effect committed.

Implement the smallest safe repair

The smallest safe repair is not to change Redis commands. It is to change the ordering and evidence around them.

A recovery worker should claim an eligible pending entry, extract the stable application work identity, consult or acquire the idempotency boundary, execute only the work that is not already complete, independently verify that completion, then XACK the stream ID. If an already-complete work identity is recovered because the prior worker finished the side effect but failed before XACK, the recovery worker should reconcile that prior completion and acknowledge without reapplying the effect.

In pseudocode:

claimed = XAUTOCLAIM(...)

for entry in claimed:
    work_id = entry.job_id

    if completion_exists(work_id):
        reconcile_existing_completion(work_id)
        XACK(entry.id)
        continue

    result = perform_idempotent_work(work_id)

    if completion_evidence_matches(work_id, result):
        XACK(entry.id)
    else:
        leave_pending_and_escalate()

The important phrase is “idempotent work,” not merely “retry.” Repeated claim is a normal recovery possibility because an entry can become idle again before acknowledgment. Redis explicitly notes that repeatedly failed messages can accumulate high attempted-delivery counts.

Do not hide a conflicting completion record by overwriting it. If job-B already exists with a different amount, destination, version, or payload hash, that is not an idempotent duplicate; it is an identity conflict. The correct decision is HOLD until the application owner determines which operation is authoritative.

Likewise, do not use JUSTID merely to avoid changing delivery counters during diagnosis unless that is intentional. Redis documents that JUSTID suppresses retry-counter increments; if delivery count is part of your evidence model, changing to JUSTID changes the measurement.

The repair should therefore be small in code but strong in evidence: stable work identity, idempotent completion boundary, completion-before-ACK ordering, and explicit reconciliation.

Test rollback, recovery and rerun safety

Rollback in this context does not mean “give the message back” as though ownership transfer were a database transaction. After XAUTOCLAIM, another consumer owns the PEL entry. More importantly, external work may already have happened. A generic rollback of Redis ownership cannot undo an email, payment, database commit, or remote API operation.

The safe recovery question is instead: what can be repeated without creating a second effect?

For C05, the answer is known because the filesystem completion marker is the synthetic idempotency boundary. A second worker can safely attempt the same logical job, discover that completion already exists, classify the attempt as a duplicate, and acknowledge after reconciliation.

For C03, by contrast, there is no completion record. The claimed message is still pending with the new owner. That state is repairable: perform the work, verify its completion, then acknowledge.

For a real system where the external side effect timed out and its commit status is unknown, neither blind replay nor blind acknowledgment is justified. Query the authoritative downstream system using the same work identity. If that query cannot distinguish completed from not completed, classify the case HOLD rather than guessing.

Rerun the entire fixture from a clean container instead of attempting to “reset” timings in place:

docker rm -f "$NAME"
rm -rf "$LAB/effects" "$LAB/evidence"
mkdir -p "$LAB/effects" "$LAB/evidence"

# Recreate the pinned container and rerun from the frozen manifest.

The methodological link to Pytest strict XPASS release gate is the review boundary: an apparently favorable status must not silently erase the exceptional state that requires evidence. The pytest article's subject is expected-failure policy; this one applies that discipline to pending Redis work and duplicate recovery.

Assign recovery ownership

Assertion: every HOLD or REPAIR case has a named operational owner for the application side effect, not merely the Redis service.

Expected outcome: the owner can decide whether existing work is authoritative, safe to retry, compensatable, or irreconcilable.

Rejection condition: if nobody owns the downstream completion decision, automated acknowledgment stays blocked.

Redis ownership and organizational recovery ownership are different things.

Apply the ACCEPT / REPAIR / RERUN / HOLD matrix

The decision gate should be mechanical enough that two reviewers examining the same evidence reach the same category.

Decision

Redis evidence

Work evidence

Required action

ACCEPT

Expected owner/claim history; required XACK; final PEL reconciles

Exactly the required completion state; duplicate attempts explained

Release the case

REPAIR

Redis behavior matches docs, but handler ordering/idempotency is defective

Effect is known and safely correctable

Fix handler and rerun affected cases

RERUN

Evidence missing, malformed, version-unpinned, or test not executed

No unresolved real-world side effect

Recreate clean fixture and execute

HOLD

Redis state may be known or unknown

External completion is ambiguous, conflicting, or unsafe to replay

Stop automated recovery; investigate authoritative system

For C01 through C08, acceptance requires case-by-case reconciliation rather than one aggregate pass. A single missing deleted-ID case, duplicate completion, unexplained PEL entry, or version mismatch prevents ACCEPT.

The current validation decision is RERUN. The reason is not a known Redis defect and not an unresolved production side effect. It is simpler: the runtime fixture was not executed in the research environment, so there are no observed case outputs to compare with the frozen manifest.

The expected release transition is:

Current state:
RERUN

After executing the pinned fixture:
    all expected identities present
    negative control demonstrates claim != completion
    positive control records completion before XACK
    duplicate attempt is blocked/reconciled
    deleted PEL entry behaves as documented
    required pending counts reconcile to zero
    no evidence conflicts
        => ACCEPT

If handler ACKs without completion evidence
        => REPAIR

If fixture output is missing or baseline differs
        => RERUN

If external effect status cannot be determined safely
        => HOLD

This gate intentionally refuses the phrase “XAUTOCLAIM succeeded, therefore recovery succeeded.” XAUTOCLAIM tells you about eligible PEL ownership. XPENDING tells you what remains pending and who owns it. XREADGROUP defines delivery and pending-history access. XACK removes acknowledged pending IDs. None of those commands independently attests that arbitrary application work outside Redis completed.

That is the release boundary: reconcile Redis delivery state with independently recorded work state, and reject any case whose two histories cannot be made consistent.

Develop related skills with Refonte Learning

This playbook requires several skills that extend beyond knowing four Redis commands: defining a recoverable work identity, building a disposable validation fixture, separating documented behavior from observed behavior, preserving raw evidence, designing idempotent processing, and making a release decision from reconciliation rather than return codes.

Refonte Learning's current Data Engineering Program describes a three-month program with a listed commitment of 12–14 hours per week. Its page names real-time data processing, data pipelining, ingesting streaming and batch data, big-data analytics, data storage, security, encryption, governance, Hadoop, and Spark among its competencies or featured areas. It also states that applicants should be engaged in bachelor's or postgraduate studies and describes training and internship certificates on successful completion.

Those program details are relevant to the broader engineering discipline behind this fixture, but the program page does not establish specialized instruction in Redis Streams, XAUTOCLAIM, PEL recovery, or this exact acknowledgment-reconciliation procedure. Do not infer that specialization from the presence of real-time processing in the curriculum.

For engineers who want structured practice around data pipelines and real-time processing, the program page is therefore a reasonable adjacent resource to review. The Redis acceptance method in this article should still be learned from the Redis primary documentation and validated against the exact runtime being operated.

The final operational conclusion remains narrow: a Redis Stream message being claimed is evidence of ownership transfer, not proof of work completion. Treat completion as an independently reconciled application fact, acknowledge only after that fact is established, and classify unreconciled cases as REPAIR, RERUN, or HOLD rather than turning a successful Redis reply into a false-positive release signal.