62.6 Redis Pub/Sub and Streams
Right, let’s talk about Redis’s two main ways of shouting into the void and hoping someone listens: Pub/Sub and Streams. One is a fire-and-forget party line from the 70s, and the other is a robust, persistent, modern messaging system. I’ll let you guess which one you should probably use for anything important.
The Party Line: Classic Pub/Sub
Redis Pub/Sub is the digital equivalent of shouting in a crowded room. You publish a message on a “channel,” and everyone currently subscribed to that channel gets it. Immediately. The key word there is currently. If a client subscribes after the message is published, it misses it. Forever. There’s no history, no persistence, no nothing. It’s the messaging equivalent of a mayfly.
It’s brilliantly simple and perfect for a narrow set of use cases: live dashboards, simple notifications, or chat rooms where missing a message isn’t a disaster. Here’s the basic incantation:
# Terminal 1 (The Listener)
SUBSCRIBE news:weather
# Terminal 2 (The Shouter)
PUBLISH news:weather "It's raining sidewise, folks."
The listener in Terminal 1 will instantly print the message. But if you start a third terminal and subscribe after the publish, it hears nothing but crickets. See what I mean? It’s all about who’s in the room right now.
The Persistent Journal: Redis Streams
Redis Streams is Pub/Sub’s older, more responsible sibling who shows up with a notepad and a pen. Instead of channels, you have streams. When you add a message (an “entry”) to a stream, it gets a unique ID (like 1678901234567-0, a timestamp-sequence combo) and it sticks around until you decide to delete it. Clients can then read from the stream at their own pace, from any point in its history.
This is the foundation for proper message queues, event sourcing, and replicating data changes. It’s what Pub/Sub should have been.
# Add an entry to a stream. The * lets Redis auto-generate an ID.
XADD mystream * sensor_id 123 temperature 22.5
# Read all entries from the start of the stream, but only return 100 at a time.
XRANGE mystream - + COUNT 100
The real magic, however, is in the blocking read. A consumer can ask for new messages starting after the last one it processed. This is the core of a reliable system.
# Read from 'mystream', starting after the ID '1678901234567-0',
# and block for up to 5000ms waiting for new messages.
XREAD BLOCK 5000 STREAMS mystream 1678901234567-0
Consumer Groups: The True Power-Up
While a single consumer reading a stream is useful, the real-world pattern is having multiple consumers working together in a consumer group to process messages from the same stream. This gives you parallelism and fault tolerance. It’s like having a team of workers pulling tasks from a shared queue.
# Create a consumer group 'mygroup' for the stream 'mystream', reading from the start.
XGROUP CREATE mystream mygroup 0
# Now, a consumer can claim a message from the group for processing.
# This is a blocking call that will wait for a new message.
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >
The > is the special ID meaning “messages I haven’t acknowledged yet.” Once consumer1 processes the message, it must send an acknowledgment (XACK) back to Redis. If the consumer crashes, the message remains in a “pending” state and can be claimed by another consumer after a timeout. This is the mechanism that prevents data loss.
The Gotchas and The Glory
Here’s the stuff the quickstart guides won’t tell you:
- Pub/Sub is a memory hog if you’re not careful. If a publisher is firing messages faster than the slowest subscriber can receive them, Redis will buffer them in memory. If a subscriber disconnects and misses a bunch of messages, those buffered messages are gone. Poof.
- Streams need housekeeping. Unlike Pub/Sub, messages in a stream live forever by default. You must decide on a retention policy. Do you keep the last 1000 messages? The last 24 hours? Use
XTRIMor theMAXLENoption withXADDto avoid your stream turning into a black hole for memory.# Add a message and trim the stream to ~1000 entries XADD mystream MAXLEN ~ 1000 * data "more important info" - The
~inMAXLENis your friend. It means “approximately,” letting Redis trim the stream in a more efficient way rather than doing a precise trim every single time. You almost always want this. - Consumer Groups require monitoring. You need to keep an eye on the Pending Entries List (PEL) with
XPENDING. A growing PEL means your consumers are failing to acknowledge messages, either because they’re too slow or they’re broken. It’s your canary in the coal mine.
So, which do you use? Use Pub/Sub for “hey, look at this right now” and accept that it’s lossy. For anything that matters—order processing, event logging, task queues—use Streams. It’s the difference between yelling “fire!” in a theater and sending a certified letter. One is faster, but you really want the receipt.