In 30 seconds
RabbitMQ is an open-source message broker. Producers publish messages to exchanges, exchanges route them into queues by rules, and consumers take messages from queues and acknowledge them when the work is done. It decouples services, smooths traffic spikes and retries failed work. The current 4.x line supports AMQP 0-9-1, AMQP 1.0, MQTT and streams, with replicated quorum queues as the safe default.
Not in my daily stack — I know the concepts and would learn the details when a project needs it.
Why it matters
- Not every task should happen inside an HTTP request. A broker lets an API accept work instantly and hand emails, image processing or integrations to background consumers.
- Routing is its strength: one event can reach many queues by type or pattern, so new consumers can be added without changing the producer.
- It is language-neutral: a PHP service can publish and Go, Python or Node.js workers can consume, which matters in polyglot systems.
- It sits between simple Redis-based job queues (such as BullMQ or Laravel queues) and a log platform like Kafka: richer routing and delivery guarantees than the former, simpler operations than the latter.
How it works
01The idea: a post office for messages
Think of RabbitMQ as a post office. A producer drops a letter (message) at the counter (exchange) with an address label (routing key). The post office sorts it into mailboxes (queues) according to rules (bindings). A consumer empties its mailbox and signs for each letter (acknowledgement). If the consumer never signs, the letter goes back to be delivered again. This separation means the producer does not know or care who handles the message, how many workers there are, or whether they are online right now; messages wait safely in the queue.
- Message: a payload (bytes, usually JSON) plus properties like content type, message ID and headers.
- Queue: an ordered buffer that stores messages until a consumer takes them.
- Connection and channel: one TCP connection per process, with lightweight channels inside it for each thread or task.
02Exchanges and routing
Producers never publish directly to a queue; they publish to an exchange. The exchange type decides routing. A direct exchange delivers to queues whose binding key equals the routing key exactly. A topic exchange matches patterns with dots: “order.*” matches “order.created”, and “order.#” matches any depth. A fanout exchange copies every message to all bound queues, ignoring keys. A headers exchange routes by header values. The common design for events is one topic exchange per domain (orders, users) and one queue per consumer purpose (email.order-created, analytics.orders), so each consumer gets its own copy and its own pace.
- Default exchange: a nameless direct exchange that routes to a queue with the same name as the routing key.
- Unroutable messages are dropped unless you publish with mandatory or configure an alternate exchange.
03Queue types: quorum, classic and streams
Quorum queues replicate data across an odd number of nodes using the Raft consensus algorithm; a message is confirmed only after a majority has it, so it survives a node failure. They are the recommended choice for anything important. Classic queues are single-node and non-replicated since RabbitMQ 4.0 removed classic mirrored queues; they are fine for temporary or reproducible work. Streams are an append-only log: consumers read from an offset and messages are not removed on consumption, which enables replay and very high fan-out, similar to Kafka topics. Quorum queues also support a delivery limit, which protects against poison messages that crash consumers repeatedly.
04Publishing reliably
Durable queues and persistent messages survive a broker restart, but they do not tell the producer that the broker got the message. For that, use publisher confirms: the broker acknowledges each message once it is safely stored (for quorum queues, replicated to a majority). If a confirm does not arrive or comes back as a nack, republish. Give each message a unique messageId so consumers can detect duplicates, because retries make delivery at-least-once. Declare exchanges and queues in code or in a definitions file, so environments are reproducible.
import amqp from "amqplib";
const conn = await amqp.connect("amqp://your_user:your_password@localhost:5672");
const ch = await conn.createConfirmChannel();
await ch.assertExchange("orders", "topic", { durable: true });
await ch.assertQueue("email.order-created", {
durable: true, arguments: { "x-queue-type": "quorum" },
});
await ch.bindQueue("email.order-created", "orders", "order.created");
ch.publish("orders", "order.created", Buffer.from(JSON.stringify({ orderId: 42 })), {
persistent: true, contentType: "application/json", messageId: "order-42-created",
});
await ch.waitForConfirms(); // throws if the broker rejected the message
05Consuming: acks, prefetch and retries
Consumers should use manual acknowledgements: ack only after the work has succeeded. If the consumer crashes before acking, RabbitMQ redelivers the message to another consumer. Prefetch (basic.qos) limits how many unacknowledged messages one consumer holds; without it, the broker pushes everything to the first consumer and the others sit idle. Start with a prefetch of 10–50 and tune. On failure, decide: requeue for a transient error, or reject without requeue so the message goes to a dead letter exchange (DLX) for inspection or delayed retry. Never ack before processing, and make handlers idempotent.
await ch.prefetch(20);
await ch.consume("email.order-created", async (msg) => {
if (!msg) return;
try {
const event = JSON.parse(msg.content.toString());
await sendOrderEmail(event.orderId); // must be idempotent
ch.ack(msg);
} catch (err) {
// false, false = do not requeue: route to the dead letter exchange
ch.nack(msg, false, false);
}
}, { noAck: false });
06Dead letters, policies and running it
A dead letter exchange receives messages that were rejected, expired by TTL or exceeded the delivery limit. Configure it with a policy rather than hard-coding queue arguments, because policies can be changed without redeclaring queues. A classic retry pattern routes failed messages to a retry queue with a TTL whose own DLX points back to the work exchange, giving delayed retries. Run RabbitMQ as a three-node cluster for quorum queues, enable the management plugin for the UI and HTTP API, expose metrics with the Prometheus plugin, and use TLS and per-application users with limited vhost permissions. Delete the default guest user, which only works from localhost anyway.
# Dead-letter failed messages and cap redeliveries for all email.* queues
rabbitmqctl set_policy email-dlx "^email\." \
'{"dead-letter-exchange":"dlx","delivery-limit":5}' --apply-to quorum_queues
rabbitmqctl add_vhost shop
rabbitmqctl add_user orders_app 'use-a-strong-password'
rabbitmqctl set_permissions -p shop orders_app '^orders.*' '^orders.*' '^(orders|email).*'
Use it when
- Background jobs and task distribution between workers written in different languages.
- Event notification where one event must reach several independent consumers with flexible routing.
- Smoothing traffic spikes so slow downstream systems (ERP, email, payment providers) are not overloaded.
- Request/reply and work queues that need per-message acknowledgement, retries and dead lettering.
Skip it when
- Long-term event storage and replay by many consumers at very high throughput; Kafka or RabbitMQ Streams fit better.
- A single monolith with simple background jobs, where a Redis-backed queue (BullMQ, Laravel queues) is simpler to run.
- Synchronous request handling where the caller needs an immediate answer; use a direct API call.
Alternatives
- Redis-based queues (BullMQ, Laravel queues)
Very simple for one application stack; fewer routing features and weaker durability guarantees.
- Apache Kafka
Distributed log for high-throughput event streaming with retention and replay; heavier to operate.
- Amazon SQS / SNS
Fully managed queues and fan-out on AWS; no servers, at-least-once delivery, limited routing.
- NATS (JetStream)
Lightweight, very fast messaging with optional persistence; simpler than Kafka for many cases.
Best practices
- Use quorum queues for important work and publish with publisher confirms.
- Use manual acks, ack only after success, and set a prefetch limit on every consumer.
- Make consumers idempotent with a unique messageId, because delivery is at-least-once.
- Configure dead letter exchanges and delivery limits through policies, and monitor the dead letter queues.
- Reuse one long-lived connection per process with channels per task; do not open a connection per message.
- Keep messages small (IDs and essentials) and store large payloads elsewhere.
- Monitor queue depth, consumer count, unacked messages and memory or disk alarms with Prometheus and Grafana.
Common mistakes
- Using auto-ack, so a crashing consumer silently loses messages.
- No prefetch limit, so one consumer hoards thousands of messages while others are idle.
- Requeueing failed messages forever, creating a hot loop of a poison message.
- Letting queues grow to millions of messages without alerting, until the broker hits its memory or disk alarm and blocks publishers.
- Opening a new connection for every publish, which exhausts resources under load.
Deep dive
07Quorum queues under the hood
A quorum queue is a Raft group: one leader and followers on other nodes, typically three or five members. Publishes go to the leader, which appends them to its log and replicates; once a majority has written the entry to disk, the leader confirms it to the publisher. If the leader's node fails, the remaining majority elects a new leader within seconds and consumers continue. Because every message is replicated and fsynced, quorum queues use more disk I/O and are best for queues that are drained regularly, not for millions of messages parked for days. They track redelivery counts, enforce a delivery limit (a default exists since 4.0), and support message priorities since 4.0. Very long queues should become streams.
- Use three members for most queues; five tolerate two failures at more cost.
- Members are placed across cluster nodes; keep nodes in separate availability zones.
08Flow control, alarms and back-pressure
RabbitMQ protects itself. When a node's memory use passes the high watermark (by default a fraction of RAM) or free disk drops below the disk limit, it raises an alarm and blocks all publishing connections cluster-wide until resources recover. Consumers keep working, which is how the system drains. Credit-based flow control also slows fast publishers that outrun queue processes. In practice, alarms are a symptom of a queue growing faster than it is consumed. Set max-length or TTL policies on queues that may grow unbounded, alert on queue depth growth rate, scale consumers, and give the broker fast disks with headroom. Clients should handle the connection.blocked notification and stop or buffer publishing.
09Retries with backoff and poison messages
Requeueing immediately retries too fast and can loop a poison message. A robust pattern uses dead lettering for delays: on failure, the consumer rejects the message, which goes via a DLX to a retry queue with a message TTL (for example 30 seconds) and no consumers; when the TTL expires, that queue dead-letters it back to the main exchange. Use several retry queues for exponential steps (30 s, 5 min, 30 min) and count attempts in a header or rely on the x-delivery-count header in quorum queues. After the last attempt, route to a parking queue that a human inspects. A delayed message exchange plugin exists too, but it is not replicated, so TTL queues are safer for critical work.
# Retry queue: holds failed messages for 30 s, then sends them back
rabbitmqctl set_policy retry-30s "^retry\.30s$" \
'{"message-ttl":30000,"dead-letter-exchange":"orders"}' --apply-to queues
# Main work queues dead-letter into the retry exchange
rabbitmqctl set_policy work-dlx "^email\." \
'{"dead-letter-exchange":"retry","delivery-limit":10}' --apply-to quorum_queues
10Exactly-once is a myth: idempotency and the outbox
Networks fail between “processed” and “acked”, and between “published” and “confirmed”, so any reliable system is at-least-once. Two patterns make that safe. The transactional outbox on the producer side: write the business change and an outbox row in the same database transaction, and let a relay publish outbox rows with confirms and mark them sent. This prevents “order saved but event lost” and “event sent but order rolled back”. The idempotent consumer on the receiving side: store processed message IDs (or use natural keys and upserts) in the same transaction as the side effect, and skip duplicates. With both, the system behaves as if delivery were exactly once.
Real-world scenarios
Queue keeps growing during a sale
Problem
During a sale, the order-email queue grows to 400,000 messages, emails arrive hours late, and the broker raises a memory alarm that blocks publishers, including checkout.
How to solve it
Short term, scale out the email consumers and check their prefetch; with prefetch 1 and a slow SMTP call, raise it to 20–50 and send in parallel within provider limits. Check the consumer utilisation metric in the management UI. Separate critical and non-critical traffic: checkout events should not share a vhost or node pressure with bulk emails, so move email to its own queue with a max-length or TTL policy. Long term, alert on queue depth growth, load test the pipeline before campaigns, and handle connection.blocked in the checkout publisher with a local outbox, so a broker alarm never fails a payment.
A poison message crashes every consumer
Problem
One malformed message makes the consumer throw an unhandled error. It is requeued, picked up by the next consumer, which crashes too, and the queue stops moving.
How to solve it
Catch errors per message and nack without requeue for errors that will never succeed, such as validation failures, so the message goes to the dead letter queue. Set a delivery-limit policy on the quorum queue (for example 5) so even crashes before nack stop after a few attempts. Add a parking queue and an alert on its depth. Log the messageId and error, fix the producer or the parser, and replay parked messages with a shovel or small script after the fix. Add a contract test that validates the message schema on both producer and consumer.
Migrating from classic mirrored queues
Problem
A team runs an old RabbitMQ 3.x cluster with classic mirrored queues and wants to upgrade to 4.x, where mirroring no longer exists.
How to solve it
First, inventory queues and policies that set ha-mode. Upgrade within 3.13 to the latest patch, enable all stable feature flags, then migrate important queues to quorum queues. Because a queue's type cannot be changed in place, use a blue-green approach: declare new quorum queues (or a new vhost), point publishers at the new exchange bindings, let consumers drain the old queues, then remove them. Check client features: global QoS and some transient or exclusive settings behave differently. Remove ha-* policies, then upgrade to 4.x and verify quorum queue leaders are balanced across nodes.
Cheat sheet
- Run locally with the management UI
docker run -d -p 5672:5672 -p 15672:15672 rabbitmq:4-management- List queues with depth and consumers
rabbitmqctl list_queues name type messages consumers- Declare a quorum queue (client argument)
{ "x-queue-type": "quorum" }- Set prefetch (Node.js amqplib)
await channel.prefetch(20);- Reject to dead letter
channel.nack(msg, false, false);- Cluster health check
rabbitmq-diagnostics check_running && rabbitmq-diagnostics check_local_alarms- Export definitions
rabbitmqctl export_definitions /tmp/definitions.json- Enable Prometheus metrics
rabbitmq-plugins enable rabbitmq_prometheus # scrape :15692/metrics- Delete the default guest user
rabbitmqctl delete_user guest
Questions & answers
Q1What is the difference between an exchange and a queue?
A queue stores messages until a consumer takes them; it is the mailbox. An exchange stores nothing; it is the sorting office that receives messages from producers and decides which queues get a copy, based on its type and the bindings. Direct exchanges match the routing key exactly, topic exchanges match patterns, fanout broadcasts to all bound queues, and headers exchanges match header values. This separation is what makes RabbitMQ flexible: producers publish to an exchange and never need to know how many queues or consumers exist, so you can add a new consumer by binding a new queue.
Q2How do you make sure messages are not lost?
Several layers must all be in place. On the broker, use durable quorum queues, which replicate to a majority of nodes, and persistent messages. On the producer side, enable publisher confirms and republish anything not confirmed. Use mandatory publishing or an alternate exchange so unroutable messages are not dropped silently. On the consumer side, use manual acknowledgements and ack only after the work is done, so a crash leads to redelivery instead of loss. Finally, monitor dead letter queues. The result is at-least-once delivery, which means consumers must be idempotent to handle the occasional duplicate.
Q3What is prefetch and how do you set it?
Prefetch, set with basic.qos, is the maximum number of unacknowledged messages the broker will push to one consumer or channel. Without a limit, the broker sends as many messages as possible to whichever consumer is connected, so one worker may hold thousands while another sits idle, and a crash returns them all at once. A low prefetch such as 1 gives fair distribution for slow, heavy tasks; a higher value such as 20 to 100 increases throughput for fast tasks by hiding network latency. Start around 10 to 50, measure throughput and consumer utilisation, and tune.
Q4RabbitMQ or Kafka?
They solve different problems. RabbitMQ is a broker focused on routing and per-message delivery: messages go to queues, consumers acknowledge them and they are removed, with flexible routing, priorities, TTLs and dead lettering. Kafka is a distributed append-only log: messages are retained for days or forever, consumers track offsets and can replay, and partitioning gives very high throughput with ordering per key. Choose RabbitMQ for task queues, workflows and integration with complex routing. Choose Kafka for event streaming, analytics pipelines and event sourcing where replay matters. RabbitMQ Streams narrow the gap for moderate replay needs.
Q5What is a dead letter exchange?
A dead letter exchange is a normal exchange that receives messages RabbitMQ could not process in their original queue: messages rejected by a consumer without requeue, messages whose TTL expired, messages dropped because the queue exceeded its length limit, and with quorum queues, messages that exceeded the delivery limit. You bind a dead letter queue to it, alert on its depth, and inspect or replay messages after fixing the bug. Combined with a TTL queue, it also implements delayed retries. It is the safety net that turns a poison message from an infinite loop into a visible, recoverable problem.
Q6Does RabbitMQ guarantee message order?
Within a single queue, messages are delivered in the order they were published, as long as there is a single consumer and no requeues. With several consumers, messages are processed in parallel, so completion order is not guaranteed, and a requeued or redelivered message can come back after later ones. If strict ordering matters per entity, such as all events for one order, route by a key to one queue per partition with the consistent hash exchange or use single active consumer on the queue. Often the better answer is to design consumers to tolerate reordering, for example by using version numbers.
Senior-level questions
S1How would you design an event-driven architecture on RabbitMQ for several services?
I would create one durable topic exchange per bounded context (orders, payments, users) owned by the publishing service, with routing keys like order.created.v1 that include a version. Each consuming service declares its own quorum queue per purpose and binds the keys it needs, so services evolve independently. Producers publish through a transactional outbox with confirms; consumers are idempotent and use manual acks, prefetch, a DLX with delayed retries and a parking queue. Message schemas live in a shared contract repository with compatibility tests. Each service gets its own user with permissions limited to its exchanges and queues. I would add tracing headers for observability. The trade-off versus Kafka is no long retention or replay, so I would add Streams or a separate log if consumers need history.
S2How do you guarantee per-entity ordering while scaling consumers?
A single queue with many consumers processes in parallel, so order breaks. Options: use single active consumer on the queue, which keeps order but gives up parallelism and acts as hot standby. To scale, partition: the consistent hash exchange routes by a hash of the order ID to N queues, each with a single active consumer, so each entity always lands on the same queue and is handled in order, and throughput scales with N. Streams with super streams provide a similar partitioned model. Redeliveries can still reorder within a partition if you requeue, so prefer retry patterns that block that partition briefly, or design consumers with version checks so a stale event is ignored. Ordering always costs throughput or complexity.
S3When would you choose RabbitMQ Streams over quorum queues?
Quorum queues are for work distribution: each message is processed by one consumer and deleted after ack, with retries and dead lettering. Streams are an append-only replicated log: messages remain until retention removes them, consumers read from any offset, and many consumers can read the same data without copies per queue. Choose streams for large fan-out (hundreds of consumers of the same events), replay after a bug or for a new consumer, very long backlogs, or high throughput via the binary stream protocol. Stay with quorum queues when you need per-message acks, delivery limits, priorities and DLX workflows. Streams do not support dead lettering, and consumers must track their own offsets, which shifts responsibility to the application.
Glossary
- Broker
- A server that receives, stores and delivers messages between applications.
- Exchange
- The routing component that receives published messages and copies them to bound queues.
- Binding
- A rule linking an exchange to a queue, often with a routing key or pattern.
- Routing key
- A label on a message that exchanges use to decide where it goes.
- Quorum queue
- A replicated queue type based on Raft that survives node failures.
- Acknowledgement
- A consumer's signal that a message was processed and can be removed.
- Prefetch
- The maximum number of unacknowledged messages delivered to one consumer at a time.
- Dead letter exchange
- An exchange that receives rejected, expired or over-retried messages.
- Publisher confirm
- The broker's acknowledgement to a producer that a message was stored safely.
- Vhost
- A virtual host: an isolated namespace of exchanges, queues and permissions.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.