The problem
Every order in Acme Shop sets off follow-up work: a confirmation email, a booking with the Thai warehouse when the order ships in Thailand, and an audit record for everything. Done inside the checkout request, that work makes the customer wait for the slowest part of it, and a slow mail server or warehouse API turns into failed checkouts. Calling each worker directly is no better: the Orders service has to know all of them, and a burst of orders reaches all of them at the same moment.
A message broker sits in between. Orders hands each event to RabbitMQ and answers the customer; RabbitMQ stores the event, works out which workers need a copy, and gives each worker its work when the worker is ready for it.
How it works
RabbitMQ implements the AMQP 0-9-1 model, and the diagram uses its vocabulary:
- Publishers send messages to an exchange, never straight to a queue. Each message carries a routing key, here words separated by dots such as
order.created.th. - The exchange's type decides how it routes. A direct exchange delivers to the queues whose binding key equals the routing key, a topic exchange matches patterns in which
*stands for exactly one word and#for zero or more, a fanout exchange copies every message to every bound queue, and a headers exchange matches message headers instead of the key. There is also a nameless default exchange that delivers to the queue named by the routing key. - A binding connects an exchange to a queue with a key or a pattern. A message that matches several bindings is copied into each of those queues. One that matches none is discarded, sent to an alternate exchange if one is configured, or returned to the publisher if it was published as
mandatory. - A queue keeps messages in order until consumers have processed them. RabbitMQ 4.x has three types: classic queues (one copy, on one node), quorum queues (replicated with Raft; the type to use when the data matters) and streams (an append-only log that consumers read without removing anything).
- Consumers subscribe, and the broker pushes messages to them, up to the number of unacknowledged messages that the consumer's prefetch (
basic.qos) allows. The consumer acknowledges withbasic.ackonce the work is done, and the message is deleted;basic.rejectandbasic.nackgive it back, to be requeued or, with requeue off, dead-lettered or dropped. If a channel or connection closes while it holds unacknowledged messages, RabbitMQ requeues them and redelivers them with theredeliveredflag set, so delivery is at least once. - Publisher confirms (
confirm.select) cover the other direction: the broker acknowledges a message once every queue it was routed to has accepted it, which for a quorum queue means a majority of its replicas.
Quorum queues bring the behaviour of steps 3 and 4. Each one has a leader and followers on different nodes (three members by default, set with x-quorum-initial-group-size), so a queue on three nodes keeps working, and keeps every confirmed message, when one node is lost. A follower that comes back resumes replication where it stopped, without the catch-up that classic mirrored queues needed; mirroring was deprecated in 2021 and removed in RabbitMQ 4.0, leaving quorum queues and streams as the replicated types.
Quorum queues also count failed deliveries in the x-delivery-count header. Since RabbitMQ 4.0 the delivery limit defaults to 20: when the count goes past it, the queue drops the message, or dead-letters it if a dead-letter exchange (DLX) is configured. Messages are also dead-lettered when a consumer rejects them without requeueing, when their TTL runs out (a message-ttl policy or the per-message expiration property) and when a length limit pushes them out; the x-death header records why.
The rest of the model, briefly:
- Priorities. Classic queues support them when declared with
x-max-priority, and the documentation recommends only a few levels. Quorum queues gained priorities in 4.0 and strict priorities with 32 levels (0 to 31) in 4.3. Streams have none. - Request-reply. The client sets
reply_to(where to send the answer) andcorrelation_id(to match the answer to its request); Direct Reply-To (amq.rabbitmq.reply-to) does without a reply queue. - Protocols. AMQP 0-9-1, and AMQP 1.0, which has been a core protocol, always enabled, since 4.0. MQTT 3.1, 3.1.1 and 5.0 and STOMP 1.0 to 1.2 come as plugins that ship with the server, and streams also have their own binary protocol.
- Clusters. Nodes share their metadata (virtual hosts, users, exchanges, queues, bindings, policies) through a metadata store. Khepri, which is based on Raft, became the default for new clusters in 4.2 and the only store in 4.3, which removed Mnesia; a majority of the nodes must now be running for the cluster to be available.
- Version and licence. The 4.3 series is current (4.3.6, released in September 2026). The server and its core plugins are open source under the Mozilla Public License 2.0.
Where it fits
- Background work behind a web or API tier, the Acme Shop case: the request publishes and returns, and workers send emails, render documents or call slow partners. This is Web-Queue-Worker, with the queue absorbing bursts as in Queue-Based Load Leveling, and with email-1 and email-2 sharing one queue as Competing Consumers.
- Routing events by content. One topic exchange and a queue for each interested service, bound to the keys it needs: Publish-Subscribe with the filtering done by the broker, a common backbone for Event-Driven Architecture.
- Request-reply between services, with
reply_toandcorrelation_id. For jobs that take longer than a client should wait, see Asynchronous Request-Reply. - Task queues for frameworks. Celery uses RabbitMQ as its default broker and can declare quorum queues (
task_default_queue_type = "quorum"). - Devices, through the MQTT plugin, publishing into the same exchanges that services consume from.
It also implements or supports Dead-Letter Queue (a DLX plus the delivery limit), Priority Queue, Idempotent Consumer (needed because delivery is at least once), Retry with Backoff (the delayed retry of quorum queues in 4.3), Claim Check for payloads above the maximum message size (16 MiB by default since 4.0) and Transactional Outbox for publishing reliably from a database transaction.
Its usual neighbours are the services that publish; the worker deployments that consume, often scaled on queue length (for example by KEDA's RabbitMQ scaler on Kubernetes); Prometheus and Grafana, through the rabbitmq_prometheus plugin that ships with the server; and a load balancer in front of the nodes.
Managed offerings: Amazon MQ for RabbitMQ supports RabbitMQ 4.3 and 4.2 (on mq.m7g instances) and 3.13 as of October 2026, either as a single-instance broker in one Availability Zone or as a cluster of three nodes across Availability Zones, behind a Network Load Balancer in both cases; it doesn't support streams. CloudAMQP is a hosted RabbitMQ service.
When to use it
Choose RabbitMQ when each message is a task or a notification that is finished once it has been processed, and you want the broker to route it, track an acknowledgement for every message and set aside the ones that keep failing: background jobs, events routed to the services that need them, request-reply.
| RabbitMQ 4.3 | Apache Kafka 4.3 | Amazon SQS | |
|---|---|---|---|
| Model | Exchanges route messages into queues | A partitioned, replicated log of records | A managed queue |
| Consumers | Pushed up to the prefetch limit, ack each message | Pull records and track their position (offset) | Poll, then delete each message when done |
| After processing | Deleted on ack (a stream keeps it) | Kept until retention ends (7 days by default) and can be re-read | Deleted by the consumer |
| Routing | Direct, topic, fanout and headers exchanges | Topic and partition, chosen by the producer | None in the queue; fan out with SNS or EventBridge |
| Order | Per queue, until several consumers or redeliveries | Per partition | Best effort (standard), per message group (FIFO) |
| Poison messages | Quorum queues: delivery limit (20 by default), then a dead-letter exchange | Up to the application; share groups cap the attempts | maxReceiveCount, then a dead-letter queue |
| Running it | Your cluster, Amazon MQ or CloudAMQP | Your brokers, or a service such as Amazon MSK | Fully managed by AWS |
Choose something else when consumers must re-read history or start again from the beginning (Kafka, or RabbitMQ streams), when you run on AWS and want plain queues with no brokers to operate (SQS, with SNS or EventBridge to fan out), or when strict order has to hold across many parallel consumers (Kafka partitions, or one single active consumer per key).
Trade-offs
- At least once. Crashes, lost connections, timeouts and requeues all lead to redelivery, so the same message can be processed twice, and consumers must be idempotent. The
redeliveredflag only says that the message may have been seen before;x-delivery-countsays how many deliveries failed. - No replay. An acknowledged message is deleted. Streams keep messages until their retention runs out and let a consumer start from any offset; classic and quorum queues do not.
- Order holds per queue, not per workload. Messages published on one channel are enqueued in that order, and a single consumer receives them in that order. Several consumers, redeliveries and priorities all change the order in which messages are processed. When the order per order ID matters, use single active consumer, or split the messages by key across several queues (the
x-modulus-hashexchange), each with one active consumer. - Long queues cost memory. A quorum queue keeps at least 32 bytes of metadata in memory for every message it holds, roughly 1 MB per 30,000 messages whatever their size. When a node's memory use reaches its high watermark (about 60% of RAM by default) or its free disk space falls below the limit (50 MB by default), RabbitMQ blocks the connections that publish until the alarm clears. Keeping queues short is the best way to keep memory use low.
- Someone has to run it. Cluster sizing, quorum queue membership, upgrades (only 4.2.x clusters can be upgraded in place to 4.3) and capacity are yours, unless a managed service takes them on.
Implementation notes
Keep the topology in code or definitions, and the tunables in policies. Applications declare their exchanges, queues and bindings, or the nodes import a definitions file at boot. Settings that operators may want to change, such as the dead-letter exchange and the delivery limit, belong in a policy rather than in hard-coded
x-arguments. For Acme Shop:rabbitmqctl set_policy orders-qq '^(email|fulfilment-th|audit)$' \ '{"dead-letter-exchange": "orders.dlx", "delivery-limit": 20}' \ --apply-to quorum_queuesA consumer, in Python with pika (the client that the RabbitMQ tutorials use):
import pika conn = pika.BlockingConnection(pika.ConnectionParameters("rabbitmq.internal")) ch = conn.channel() ch.exchange_declare("orders", exchange_type="topic", durable=True) ch.queue_declare("email", durable=True, arguments={"x-queue-type": "quorum"}) ch.queue_bind("email", "orders", routing_key="order.created.*") ch.basic_qos(prefetch_count=10) # at most 10 unacked deliveries to this consumer def on_message(ch, method, properties, body): try: send_email(body) # may run twice for one order: make it idempotent except TemporaryFailure: ch.basic_reject(method.delivery_tag, requeue=True) # counts toward delivery-limit else: ch.basic_ack(method.delivery_tag) ch.basic_consume("email", on_message) ch.start_consuming()A publisher calls
ch.confirm_delivery()once; from then onbasic_publishwaits for the broker's confirm, and raises an exception if the broker nacks the message, or returns it because it was published withmandatory=Trueand matched no queue.Reject, nack and the delivery limit. Since 4.3 only real failures count toward the limit:
basic.reject, a consumer that crashes and a lost connection. Abasic.nackwith requeue, or a consumer timeout, returns the message without raisingx-delivery-count, so a consumer that nacks a poison message over and over never reaches the limit. 4.3 also adds delayed retry for quorum queues (delayed-retry-type,delayed-retry-min,delayed-retry-max), which keeps a returned message out of circulation for longer after each failure.Dead-lettering is at most once by default for quorum queues. Where every dead letter matters, set
dead-letter-strategytoat-least-once, which also needsoverflowset toreject-publish.Prefetch. Keep it low for slow jobs (1 for the fulfilment and audit workers here, whose jobs take seconds) and higher for quick ones. Quorum queues cap it at 2,000, and the documentation notes that a prefetch of 2,000 gives about the same consumer throughput as 300. Quorum queues do not support a global (per-channel) prefetch, so set it per consumer.
Consumer timeout. A delivery left unacknowledged for 30 minutes (the default) is returned to the queue. From 4.3 the timeout applies to quorum queues only, and it can be set per queue or per consumer.
What to watch. Per queue,
messages_ready(waiting) andmessages_unacknowledged(delivered, not yet acked), for example withrabbitmqctl list_queues name messages_ready messages_unacknowledgedor the Prometheus metrics; the number of consumers; memory and disk alarms; and connection and channel churn. Connections and channels are meant to be long-lived: open them once and reuse them, rather than opening one per message.Sizing quorum queues. Three members tolerate the loss of one node and five tolerate two, while a fourth member adds no tolerance over three. All the quorum queues on a node share a write-ahead log that can hold 512 MiB by default (
raft.wal_max_size_bytes), and the documentation suggests giving each node at least three to four times that much memory.