Sign in
← Data
12 min · medium · Lesson 5 of 5

Message queues: why and how

Why you decouple services with a queue, how a producer differs from a consumer, and what sets RabbitMQ apart from Kafka.

The problem with calling directly

After an order is placed, the orders service needs to send an email, credit bonuses, and update analytics. If it calls these three services directly and waits for a reply, it becomes slow and fragile: the email service goes down, and the order can't be placed, even though it has nothing to do with email. A message queue decouples this.

Producer, queue, consumer

The idea is simple. A service doesn't call others directly, it puts a message into a queue and moves on. Other services pick messages up from the queue and process them at their own pace.

"order created" pick up producerorders queue consumers email bonuses analytics
The producer is free right away, and the consumer works through the queue at its own pace.

The orders service no longer waits on email and no longer crashes because of it: it dropped off a message and is free.

What this buys you

There's a cost too. The broker becomes one more service you need to keep alive and monitor. And the result of processing doesn't arrive right away: the email goes out a second later or a minute later, depending on the queue.

Acknowledgment: a message won't get lost

A consumer picks up a message and crashes mid-processing. If the broker already deleted the message, it's lost. That's why brokers wait for an acknowledgment (ack): the consumer reports "processed", and only then is the message considered delivered.

Until the acknowledgment arrives, the message sits as unacknowledged. If the consumer died, the broker hands this message out again, either to the same consumer after restart or to another one. In RabbitMQ, unacknowledged messages go back to the queue when the consumer's channel closes.

Hence the rule: acknowledge a message after the work is done, not before. Acknowledge before processing and then crash, and the message is lost.

At-least-once and idempotency

Acknowledgment protects against loss, but it creates repeats. A consumer credits a bonus, crashes before acknowledging, and after restart gets the same message a second time. This guarantee is called at-least-once: a message gets through at least once, but it may arrive again.

So a consumer needs to be idempotent, like the API from the idempotency lesson: processing the same message twice shouldn't credit the bonus twice. The RabbitMQ docs explicitly require consumers to be ready for redelivery.

A common approach: the message carries its own ID (an order number, an event ID), and the consumer remembers the IDs it has handled. An ID it has already seen means the consumer skips the work and just acknowledges the message. In Redis, remembering an ID is convenient with SET ... NX: it writes the key only if it doesn't exist yet.

SET done:event:77 1 NX    -- OK: first time, do the work
SET done:event:77 1 NX    -- (nil): already processed, skip

Best of all is when the "processed" marker and the work itself change together, in one transaction or one script. Otherwise there's a narrow window left: the marker is set, a crash happens before the work, and the message is skipped for good.

Queues by hand: Redis Streams

We'll handle queues in the labs with Redis, which you already know from the previous lesson. It has a data type called stream: a log of messages that get appended to the end. Each message gets an ID like 1791816841957-0 (a timestamp in milliseconds and a sequence number) and a set of fields.

XADD events * type signup user 7     -- append a message, * means "pick the ID yourself"
XLEN events                          -- how many messages in the stream
XRANGE events - +                    -- show them all, from first to last

Readers are organized into consumer groups. A group remembers how far it has read, and hands messages out to its consumers: each message within a group goes to exactly one consumer.

XGROUP CREATE events mailer 0                            -- group mailer, read from the very start
XREADGROUP GROUP mailer mailer-1 COUNT 10 STREAMS events >
XACK events mailer 1791816841957-0                        -- acknowledge by ID

XREADGROUP has two special values at the end. > asks for new messages the group hasn't handed out to anyone yet. 0 asks for its own unacknowledged ones: that's how a consumer, after a restart, picks up what it didn't manage to acknowledge.

A group's unacknowledged messages show up through XPENDING, and group state through XINFO GROUPS:

XPENDING events mailer           -- how many are pending and who has them
XINFO GROUPS events              -- per group: pending, last-delivered-id, entries-read, lag

Here pending is the number of messages handed out but not acknowledged. entries-read shows how many messages the group has read, and lag how many are still waiting for it in the stream. If a consumer dies for good, another one picks up its unacknowledged messages with XAUTOCLAIM.

After reading, a message doesn't disappear from the stream. Two groups on the same stream read it independently, and each gets every message. This is closer to Kafka than to a classic queue, which is exactly why a stream is a convenient way to show both approaches.

RabbitMQ and Kafka

The two most common brokers solve slightly different problems:

If you need a task queue, you usually reach for RabbitMQ. If you need an event stream for many consumers, you reach for Kafka. The line isn't hard: RabbitMQ has a stream queue type with log-like retention, and Redis Streams can do both at a smaller scale.

A mapping of concepts, so you can carry experience over from the labs:

Redis StreamsRabbitMQKafka
streamqueuetopic
consumer groupseveral consumers on one queueconsumer group
XACKbasic.ackcommit offset

Common mistakes

What you'll need for the tasks

This wraps up the data track: database, migrations, pools, cache, and queues.

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 3 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson data-basics-05-queues-basics