Redis 📂 Messaging and real-time · 2 of 2 22 min read

Redis Consumer Groups and a Real-Time Notification Feed

A practical Redis tutorial. Learn consumer groups for reliable message processing: the pending list, XREADGROUP, XACK, reclaiming stuck messages with XAUTOCLAIM, and dead-letter handling. Then build a real-time notification feed using a per-user stream, an unread counter, and a Pub/Sub live push. Includes animated diagrams and golden rules.

Section 01

A Mailroom That Never Drops a Letter

Sorting the Mail With a Team
A busy mailroom has one pile of letters and several clerks. Each letter goes to exactly one clerk, so the work is shared and no letter is handled twice. When a clerk finishes a letter, they stamp it "done". If a clerk goes home sick holding a letter, it is not lost — the supervisor gives it to someone else.

A Redis consumer group is that mailroom. Many workers read one stream, each message goes to one worker, and a message is only cleared once a worker confirms it. This tutorial covers that reliable processing, then uses it to build a real-time notification feed.
🤝
Shared work
split the load
Add more workers to a group and the messages are split between them. Throughput goes up without changing the producer.
🚫
No duplicates
one worker each
Each new message is handed to just one worker in the group. Two workers never process the same message at the same time.
🛡️
No loss
at least once
A message stays "pending" until a worker acknowledges it. If a worker dies, another can claim and finish the message.

Section 02

The Deliver-and-Acknowledge Cycle

A consumer group tracks a Pending Entries List (the PEL). When a worker reads a new message with >, Redis marks it delivered and puts it in the PEL. The message stays there until the worker sends XACK. Only then is it considered done.

A message is delivered to one worker and held in the pending list. It is removed only after the worker acknowledges it, so a crash never loses work.

📩 Deliver → Pending → Acknowledge
notifications 1761-0 1762-0 1763-0 group: g1 PEL — delivered, not acked 1762-0 → worker-1 (waiting ack) safe here even if the worker dies worker-1 processing 1762-0 ... then: XACK ① XREADGROUP > ③ XACK → remove from PEL
# create the group, starting from new messages ($). MKSTREAM makes the stream if needed
XGROUP CREATE notifications g1 $ MKSTREAM

# a worker reads NEW messages ( > ), up to 10, waiting up to 5s
XREADGROUP GROUP g1 worker-1 COUNT 10 BLOCK 5000 STREAMS notifications >

# after the work is done, acknowledge so it leaves the pending list
XACK notifications g1 1762-0

# see what is still pending for the group
XPENDING notifications g1
🧠
"At Least Once", Not "Exactly Once"

A message may be delivered again if a worker fails before XACK. That is "at-least-once" delivery. So make your work idempotent: running it twice should be safe. For a notification, check "already sent?" before sending again.


Section 03

When a Worker Crashes — Claim and Recover

A worker can die mid-message. Its message is still in the pending list, owned by the dead worker, its idle time growing. Another worker notices and takes it over with XAUTOCLAIM (or XCLAIM). Nothing is lost.

worker-1 crashed holding 1762-0. It has been idle too long, so worker-2 claims it from the pending list and finishes the job.

🔄 Reclaim a Stuck Message
worker-1 ✗ crashed was holding 1762-0 no XACK sent pending list (group g1) 1762-0 owner: worker-1 · idle: 70s worker-2 claims & finishes then XACK stuck XAUTOCLAIM a message idle longer than the limit is reclaimed — work is never lost
# find messages idle longer than 60s and hand them to worker-2
XAUTOCLAIM notifications g1 worker-2 60000 0

# or claim one specific message by ID
XCLAIM notifications g1 worker-2 60000 1762-0

# inspect pending details: how many times each was delivered
XPENDING notifications g1 - + 10
⚠️
Send Poison Messages to a Dead-Letter Stream

Some messages fail again and again. XPENDING shows a delivery count. If a message has been tried too many times (say more than 5), stop retrying: copy it to a separate "dead-letter" stream with XADD, then XACK the original so it does not block the group.


Section 04

Building a Real-Time Notification Feed

Now let us use these pieces. A notification feed must do two things at once: store every notification so a user can see it later, and push it live to a user who is online right now. We use a stream for storage and Pub/Sub for the live ping.

One event writes to the user's stream (durable) and fires a live ping. Online users get it instantly; offline users read the stored feed when they return.

🔔 Feed Architecture — Store and Push
Asha likes post 9001 App XADD + INCR + PUBLISH notif:user:88 (stream) durable history unread counter +1 PUBLISH user:88 live "new" ping User 88 online instant via WebSocket User 88 offline opens app later reads stored feed ① store ② ping push now read later
01
Store the notification
On an event, XADD it to the target user's stream notif:user:88. This is the durable copy.
02
Bump the unread count
INCR unread:user:88 so the little red badge shows the right number at once.
03
Ping live clients
PUBLISH user:88 "new". If the user is online, their WebSocket server pushes the notification instantly.
04
Catch up on return
An offline user opens the app and reads the stored feed with XREVRANGE. Nothing was missed.

Section 05

The Feed Data Model and Commands

Keep one stream per user for their notifications, a counter for unread, and a Pub/Sub channel for the live ping. Reading the feed is a reverse range; marking it read resets the counter.

# 1) a new notification for user 88 (store it, durably)
XADD notif:user:88 MAXLEN ~ 200 * type "like" actor 57 post 9001

# 2) bump the unread badge
INCR unread:user:88

# 3) ping any live client for this user
PUBLISH user:88 "new"

# --- when the user opens their notifications ---

# newest 20 notifications, newest first
XREVRANGE notif:user:88 + - COUNT 20

# mark all as read: reset the unread badge to 0
SET unread:user:88 0

# show the badge number any time
GET unread:user:88
OUTPUT of XREVRANGE notif:user:88 + - COUNT 3
1) "1763-0" type "comment" actor 91 post 9001 2) "1762-0" type "like" actor 57 post 9001 3) "1761-0" type "follow" actor 12
NeedRedis keyCommand
Store a notificationnotif:user:88 (stream)XADD ... MAXLEN ~ 200
Unread badgeunread:user:88 (string)INCR / SET 0
Live pushuser:88 (channel)PUBLISH / SUBSCRIBE
Read the feednotif:user:88XREVRANGE + - COUNT n
⚖️ Fan-Out Choice: Where to Write
Per user
Write to each recipient's own stream (fan-out on write). Simple, and reads are one stream. Best for most apps.
Shared + group
One big stream plus a consumer group that routes to workers. Better when one event fans out to millions.
Hybrid
Per-user streams for normal accounts, and a shared "fan-out worker" for a few huge accounts (a celebrity problem).
✂️
Cap Each Feed with MAXLEN

A user rarely scrolls past the last couple of hundred notifications. Add MAXLEN ~ 200 to XADD so each feed stays small and memory stays flat. Older notifications fall off the end automatically.


Section 06

Putting It Together

Here is the whole flow when Asha comments on Ben's post. The comment is saved in your database as usual; the notification uses Redis for speed and live delivery.

# after saving the comment in your main database ...

# build Ben's notification (user 88), keep his feed capped
XADD notif:user:88 MAXLEN ~ 200 * type "comment" actor 57 post 9001 text "Nice shot!"
INCR unread:user:88
PUBLISH user:88 "new"

# Ben's phone is online: its WebSocket server is subscribed and pushes at once
# Ben's laptop is offline: next time it opens, it calls XREVRANGE and sees the comment
OUTPUT — what Ben sees
online phone -> toast pops up instantly: "Asha commented: Nice shot!" red badge -> unread = 1 offline laptop-> opens later, feed already shows the comment
🎯
Reliable and Real-Time at Once

The stream makes the feed reliable: it survives a restart and offline users catch up. Pub/Sub makes it feel instant for online users. Consumer groups let you process heavy events (emails, push, digests) with many workers and never lose one.


Section 07

Golden Rules

📩 Consumer Groups & Feeds — Non-Negotiable Rules
1
Always XACK when done. A message stays in the pending list until it is acknowledged. No ack means it is redelivered later.
2
Make work idempotent. Delivery is at-least-once, so a message can arrive twice. Doing it twice must be safe — check "already done?" first.
3
Reclaim stuck messages. Use XAUTOCLAIM to take over messages a dead worker left pending, so nothing waits forever.
4
Dead-letter the poison messages. Watch the delivery count. After a few failures, move the message to a dead-letter stream and ack the original.
5
Store the feed, ping for live. Use a stream for durable notifications and Pub/Sub for the instant push. Together they are reliable and real-time.
6
Cap each user's feed. Add MAXLEN ~ 200 so notification streams stay small and memory stays flat.
7
Track unread with a counter. INCR on a new notification, SET 0 when the user opens the feed. The badge is then a single fast read.
You have completed Messaging and real-time. View all sections →