Redis Acknowledge & Trim Streams
Learn how to acknowledge messages and trim streams in Redis for efficient stream management. This lesson covers the core concepts, step-by-step instructions, hands-on exercises, and troubleshooting for Redis Streams acknowledgment and trimming.
Focus: acknowledge and trim streams
Streams are one of Redis's most powerful data structures—but without proper acknowledgment and trimming, they can quickly become a memory drain and a source of message replay nightmares. This lesson teaches you how to clean up after yourself using XACK to confirm message consumption and XTRIM to cap stream length, keeping your streams lean, efficient, and production-ready.
The problem this lesson solves
Imagine you're building a real-time event pipeline—order updates, user activity, or IoT sensor readings—using Redis Streams. Your consumers read new messages, process them, and move on. But what happens when a consumer crashes midway? Without a way to mark messages as "done," you risk either replaying processed events (duplicate side effects) or losing unprocessed events forever. Worse, streams grow unbounded: a high-throughput stream can balloon to gigabytes, exhausting memory and degrading performance.
This lesson solves two concrete problems:
- Message acknowledgment: Letting Redis know which messages a consumer group has successfully processed, so they won't be redelivered.
- Stream trimming: Removing old messages from a stream to control memory usage, while preserving enough history for late-arriving consumers.
By the end, you'll treat stream management as a first-class operation, not an afterthought.
Core concept / mental model
Think of a Redis Stream as a persistent log—like a publish‑subscribe channel but with a durable, ordered message history. Each consumer group acts like a bookmark that tracks which messages its members have read. But reading a message does not automatically mean it's processed.
XACKis the "I've handled this message" signal. Without it, the group's pending entries list (PEL) keeps the message, and another consumer may claim it if the first one fails.XTRIMis the memory cap. It lets you bound the stream to a maximum length (e.g., 10,000 entries) by evicting the oldest messages. Crucially,XTRIMdoes not delete pending messages—they remain in the PEL even if the stream entry is gone, which can cause dead-letter scenarios.
Diagram in words:
Stream: [msg1] [msg2] [msg3] [msg4] ... [msg1000]
↑ ↑
Consumer reads msg2 XTRIM cuts here
→ XACK msg2 (oldest entries removed)
Without XACK, the PEL grows unbounded, and consumers may re‑read stale messages. Without XTRIM, the stream itself grows without bound.
How it works step by step
Acknowledgment and trimming happen in distinct but complementary workflows:
1. Consumer group setup
Before you can acknowledge messages, you must have a consumer group created with XGROUP CREATE.
2. Reading messages
Consumers read messages using XREADGROUP. Redis returns only messages that haven't been delivered yet (unless you use the > special ID).
3. Processing logic
Your application processes the message—e.g., sends an email, updates a database, triggers a webhook.
4. Acknowledging with XACK
After successful processing, call XACK <stream> <group> <message-id> to remove the message from the group's PEL. Now Redis knows the message is handled.
5. Trimming with XTRIM
Periodically (or after each batch), use XTRIM <stream> MAXLEN [~] <count> to evict the oldest entries. The ~ flag (tilde) allows Redis to perform a more efficient, lazy eviction—good for production.
6. Monitoring pending entries
Use XPENDING <stream> <group> to see which messages remain unacknowledged. This is critical for detecting stuck consumers or messages that need manual reprocessing.
Hands-on walkthrough
Let's put it all together. We'll create a stream for order events, add a few messages, read them with a consumer group, acknowledge them, and then trim the stream.
Prerequisites
Make sure Redis (7.0+) is running locally. You'll use redis-cli and optionally a Python script with the redis-py library.
1. Create a stream and consumer group
# Add an event to the stream (creates the stream if absent)
XADD orders * event new_order user_id 42 amount 29.99
XADD orders * event payment_complete user_id 42 txn_id abc123
# Create a consumer group
XGROUP CREATE orders order-processors $ MKSTREAM
$ means "start reading from the newest message." MKSTREAM creates the stream if it doesn't exist.
2. Read messages with the group
# Consumer 'worker-1' reads the next unread message
XREADGROUP GROUP order-processors worker-1 COUNT 1 STREAMS orders >
Expected output (IDs will differ):
1) 1) "orders"
2) 1) 1) "1700000000000-0"
2) 1) "event"
2) "new_order"
3) "user_id"
4) "42"
5) "amount"
6) "29.99"
3. Acknowledge the message
Once the message is processed, acknowledge it:
XACK orders order-processors 1700000000000-0
Expected output: (integer) 1 — one message acknowledged.
4. Check pending entries
XPENDING orders order-processors
Expected output: 1) (integer) 0 (if you acknowledged the only message).
5. Add more messages and trim the stream
# Add 100 events quickly (simulated in CLI)
for i in $(seq 1 100); do
redis-cli XADD orders * event heartbeat seq $i
done
# Trim to the last 50 entries
XTRIM orders MAXLEN 50
Expected output: (integer) 52 — Redis removed 52 old entries (the first new_order and payment_complete plus the first 50 heartbeats, keeping the newest 50).
6. Python equivalent using redis-py
import redis
r = redis.Redis(decode_responses=True)
# Add messages
r.xadd('orders', {'event': 'new_order', 'user_id': '42'})
r.xadd('orders', {'event': 'payment_complete', 'user_id': '42'})
# Create group
r.xgroup_create('orders', 'order-processors', id='$', mkstream=True)
# Read
results = r.xreadgroup('order-processors', 'worker-1', {'orders': '>'}, count=1)
for stream, messages in results:
for msg_id, msg_data in messages:
print(f"Processing {msg_id}: {msg_data}")
# Simulate processing
r.xack('orders', 'order-processors', msg_id)
# Trim
r.xtrim('orders', maxlen=10)
Expected output:
Processing 1700000000000-0: {'event': 'new_order', 'user_id': '42'}
Pro tip: Always
XACKas close to the end of processing as possible. If you acknowledge before the real work (e.g., before a database write), a crash may cause data loss. If you acknowledge too late, other consumers may claim and re-process.
Compare options / when to choose what
| Operation | What it does | When to use | Risk if skipped |
|---|---|---|---|
XACK |
Removes a message from the consumer group's PEL | After successfully processing a message | Messages remain in PEL forever; group will re-deliver them |
XTRIM |
Removes old entries from the stream itself | Periodically or after a batch of consumption | Stream grows unbounded; memory pressure |
XDEL |
Deletes a specific message by ID | Rare; manual cleanup of a poisoned message | N/A (manual) |
XGROUP DELCONSUMER |
Removes a consumer from a group | When a consumer shuts down permanently | PEL entries assigned to that consumer become orphaned |
Key takeaway: XACK is about consumption state; XTRIM is about stream size. You need both.
Troubleshooting & edge cases
1. "I acknowledged a message, but it still appears in XPENDING"
- Cause: You're checking the wrong consumer group or stream name (case-sensitive in Redis).
- Fix: Double-check group and stream names. Use
XINFO STREAM ordersto list groups.
2. "XACK returns 0 (integer)"
- Cause: The message ID you provided is not in the group's PEL. Maybe it was already acknowledged, or it belongs to a different group.
- Fix: Confirm the message ID with
XPENDING orders order-processors. Ensure you're using the exact ID (including timestamp and sequence).
3. "I trimmed the stream, but pending messages disappeared!"
- Cause:
XTRIMdeleted the underlying stream entries, but the pending entries in the PEL remain. They become orphaned—Redis tracks them but the data is gone. - Fix: Never trim a stream that has unacknowledged messages you care about. Either acknowledge first or use a larger trim limit. Check
XPENDINGbefore trimming.
4. "My stream is still huge after XTRIM MAXLEN 10000"
- Cause:
MAXLENwithout the tilde (~) is exact but slow; with~it's approximate but faster. Also,XTRIMremoves only enough to reach the target—if the stream is smaller, nothing happens. - Fix: Use
XTRIM orders MAXLEN ~ 10000for production. Verify withXLEN orders.
5. "A consumer group claims a message, but the original consumer is still working on it"
- Cause: Another consumer used
XCLAIMto take over a pending message that was idle for too long. - Fix: Increase the idle timeout in
XCLAIMor useXAUTOCLAIMwith a sensible limit. Never auto-claim without ensuring the original consumer is truly dead.
What you learned & what's next
You now understand the critical difference between reading a message and acknowledging it. You've seen how XACK keeps your consumer groups clean and how XTRIM keeps your streams memory‑efficient. You've also discovered common pitfalls like orphaned pending entries and incorrect group names.
Learning objectives achieved:
✅ Explain the core idea behind XACK (per‑message acknowledgment) and XTRIM (stream length bounding).
✅ Complete a practical exercise using both CLI and Python.
What's next: Consumer groups often need to handle failures gracefully. The next lesson, Claiming and Auto‑claiming Messages, teaches you how to reassign unprocessed messages from dead consumers, ensuring no event is ever lost.
Practice recap
Quick exercise: Create a stream named notifications, add 20 messages, then create a consumer group admin-group. Read 5 messages, acknowledge them, then trim the stream to 10 entries. Verify with XLEN that only 10 entries remain. Use XPENDING to confirm no pending entries for the acknowledged messages.
Common mistakes
- Forgetting to create the consumer group (
XGROUP CREATE) before trying toXACK— results inNOGROUPerror. - Acknowledging a message with the wrong stream name or group name (case-sensitive) — returns
0and the message stays in PEL. - Trimming the stream using
XTRIMwithout checking pending entries first — can orphan unacknowledged messages in the PEL, leading to lost data. - Using
XTRIMwithout the~flag in high-throughput pipelines — causes O(N) blocking trimming, hurting performance.
Variations
- Use
XAUTOCLAIMinstead of manualXACK+XCLAIMfor automated retry of failed messages in consumer groups. - Use
XDELfor single-message deletion instead ofXTRIMwhen you need to remove a specific poisoned message. - Combine
XLENmonitoring with an eviction policy (e.g., MAXMEMORY allkeys-lru) instead of explicitXTRIMfor memory-only streams.
Real-world use cases
- A payment processing pipeline where each order event must be acknowledged to avoid duplicate charges —
XACKensures idempotency. - An IoT sensor data stream that grows millions of entries per hour —
XTRIM MAXLEN ~ 100000keeps memory in check while preserving recent readings. - A microservice event bus where late-arriving consumers need access to recent history — trimming to a few thousand entries provides a bounded replay window.
Key takeaways
XACKremoves a message from the consumer group's pending list, preventing re-delivery — always call it after successful processing.XTRIMbounds the stream's length; use the~flag for efficient lazy trimming in production.- Check
XPENDINGbefore any mass trim to avoid creating orphaned pending entries. - Stream management requires both acknowledgment (consumption state) and trimming (memory) — treat them as twin responsibilities.
- Use
XINFO STREAMandXINFO GROUPSto inspect stream health and pending counts.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.