RabbitMQ Explained
Route messages reliably between services using producers, exchanges, bindings, and queues.
Producer → Exchange → Queue → Consumer — AMQP 0-9-1 message flow
The Four Actors
Every RabbitMQ topology is built from the same four building blocks.
Any application that publishes messages to an exchange. Producers specify a routing key and optional headers. They don't know which queues exist — that's the exchange's job.
channel.basic_publish(
exchange='orders',
routing_key='order.created',
body=b'{"id": 42}'
)
Receives messages and routes them to zero or more queues based on its type and binding rules. An unroutable message is silently discarded — or returned to the producer if mandatory=True.
- Types: direct, fanout, topic, headers
- Can be durable or transient
- Can be internal (producer-invisible)
An ordered buffer that stores messages until a consumer is ready. Each queue is independent — a slow consumer in one queue doesn't stall another.
durable: survives broker restartexclusive: single connection onlyauto-delete: removed when unused
An application that subscribes to a queue and processes messages. Sends an acknowledgment (ack) when done, telling the broker it's safe to permanently delete the message.
def on_message(ch, method, props, body):
process(body)
ch.basic_ack(
delivery_tag=method.delivery_tag
)
A binding is a rule that tells an exchange to route messages to a specific queue. Bindings are created explicitly — queues don't automatically receive messages just because they exist. The binding can include a binding key (for direct and topic exchanges) or a headers map (for headers exchanges).
# Bind queue 'billing' to exchange 'payments' for routing key 'billing'
channel.queue_bind(
queue='billing',
exchange='payments',
routing_key='billing'
)
Message Anatomy
Every AMQP message has two parts: routing metadata and an opaque payload body.
"order_id": 42,
"user_id": "u-789",
"items": [...]
}
Persistent — written to disk before delivery. Survives a broker crash.
Ties a reply to its originating request. Essential for RPC patterns.
Queue name for the consumer to send a response back to the caller.
Connections & Channels
Opening a TCP connection per operation is expensive. AMQP multiplexes a single TCP socket into many lightweight virtual channels.
A persistent TCP socket to the broker. Creating one takes ~100ms. Reuse it for the lifetime of your process. One connection per process is the standard pattern.
A virtual sub-connection inside a TCP socket. Cheap to create. Not thread-safe — use one channel per thread. Typically: one channel to publish, one to consume.
Acknowledgments
Acks tell the broker a message was successfully processed. Without an ack, RabbitMQ keeps the message and redelivers it if the consumer disconnects.
Success. The broker permanently removes the message. Set multiple=True to ack all unacknowledged messages up to this delivery tag in one call.
channel.basic_ack(
delivery_tag=tag
)
Failure. Use requeue=True to put the message back, or False to discard it (routing it to the dead-letter exchange if configured).
channel.basic_nack(
delivery_tag=tag,
requeue=False
)
auto_ack=True in production. It acknowledges immediately on delivery — before your code runs. If your process crashes, the message is permanently lost with no trace.
The exchange type is set at declaration time and determines routing logic. It cannot be changed without deleting and recreating the exchange.
Routes a message to every queue whose binding key exactly matches the routing key. Multiple queues can share the same binding key, getting identical copies. The default exchange is a pre-declared nameless direct exchange — binding to it uses the queue name as the routing key.
# Declare and bind
channel.exchange_declare('payments', exchange_type='direct')
channel.queue_bind('billing', 'payments', routing_key='billing')
channel.queue_bind('shipping', 'payments', routing_key='shipping')
# Only the 'billing' queue receives this message
channel.basic_publish('payments', routing_key='billing', body=msg)
Delivers a copy of every message to all queues bound to it. The routing key is completely ignored. Each subscriber receives every event independently — a slow consumer's queue fills up independently of a fast one's.
# Fanout ignores routing key completely
channel.exchange_declare('events', exchange_type='fanout')
channel.queue_bind('analytics', 'events')
channel.queue_bind('notifications', 'events')
channel.queue_bind('audit-log', 'events')
channel.basic_publish('events', routing_key='', body=msg)
# All three queues receive an independent copy
Routes based on pattern matching against a dot-segmented routing key like usa.stocks.news. Binding patterns use two wildcards:
Matches exactly one word. *.error matches app.error but not app.db.error.
Matches zero or more words. app.# matches app.error, app.db.error, and app.
# Topic exchange — wildcard binding patterns
channel.exchange_declare('market', exchange_type='topic')
channel.queue_bind('stocks-feed', 'market', routing_key='*.stocks.*')
channel.queue_bind('usa-all', 'market', routing_key='usa.#')
channel.queue_bind('europe-all', 'market', routing_key='europe.#')
# 'usa.stocks.news' → matches *.stocks.* AND usa.# → 2 queues
channel.basic_publish('market', routing_key='usa.stocks.news', body=msg)
Ignores the routing key entirely. Routes based on message header values. Each binding specifies a set of header key-value pairs plus an x-match argument — all (AND) or any (OR).
ALL headers must match. Precise targeting — use for narrowly scoped consumers.
ANY header must match. Broader routing — use when one attribute is sufficient.
# Headers exchange — routing key is ignored
channel.exchange_declare('reports', exchange_type='headers')
channel.queue_bind('eu-pdf-queue', 'reports', arguments={
'x-match': 'all', # both headers must match
'format': 'pdf',
'region': 'eu',
})
channel.basic_publish('reports', routing_key='', body=msg,
properties=pika.BasicProperties(
headers={'format': 'pdf', 'region': 'eu'}))
eu.pdf.report for the same result with better observability.
At a glance
| Type | Routes on | Match logic | Best for |
|---|---|---|---|
| direct | routing key | exact string | task routing, RPC |
| fanout | (ignored) | all bound queues | broadcast, pub/sub |
| topic | routing key | wildcard (* #) | selective broadcast |
| headers | headers map | all/any attributes | attribute routing |
These patterns combine exchange types, queue configuration, and consumer behavior to solve common distributed systems problems.
Multiple workers subscribe to the same queue. RabbitMQ dispatches each message to exactly one worker using round-robin. Add more workers to scale throughput linearly. Perfect for CPU-heavy jobs: image processing, report generation, sending emails.
# All workers subscribe to the same queue
channel.basic_qos(prefetch_count=1) # fair dispatch — one in-flight msg per worker
channel.basic_consume('task_queue', on_message_callback=do_work, auto_ack=False)
Each subscriber has its own private queue bound to a fanout exchange. Every subscriber receives every event, independently. A slow subscriber doesn't delay a fast one — their queues are completely separate. Built on top of the fanout exchange.
Work Queue (wrong for pub/sub)
Shared queue → messages split among consumers. Each message processed by only one worker.
Pub/Sub (correct)
Own queue per subscriber → all subscribers get all messages. Fanout broadcasts copies.
# Each subscriber declares its own exclusive, auto-delete queue
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue
channel.queue_bind(queue_name, exchange='events')
channel.basic_consume(queue_name, on_message_callback=handler)
Enables synchronous-looking remote calls over async messaging. The client publishes a request and blocks on a reply queue. A correlation_id ties each response back to its request, allowing a single reply queue to handle many concurrent calls.
amq.rabbitmq.reply-to for the direct reply shortcut). Generates a unique correlation_id UUID.reply_to and correlation_id properties set. Then blocks, consuming from the reply queue.reply_to with the same correlation_id. Acks the original request.correlation_id matches the pending request. If not, discards it (stale reply from a previous call). If yes, unblocks and returns the result.# RPC client — using amq.rabbitmq.reply-to for zero-latency direct reply
corr_id = str(uuid.uuid4())
channel.basic_publish(
exchange='',
routing_key='rpc_queue',
body=json.dumps({'n': 35}),
properties=pika.BasicProperties(
reply_to='amq.rabbitmq.reply-to',
correlation_id=corr_id,
),
)
When a message becomes "dead", RabbitMQ can route it to a Dead Letter Exchange (DLX) rather than silently discarding it. From there it routes to a dead letter queue for inspection, alerting, or retry. There are four causes:
Consumer explicitly rejects a message as unprocessable (basic.nack or basic.reject with requeue=false).
Message sits in queue longer than the per-message or per-queue TTL (x-message-ttl).
Queue exceeds x-max-length (or x-max-length-bytes) — the message at the head of the queue is dropped and dead-lettered.
Message is redelivered to a quorum queue more times than its x-delivery-limit. Applies to quorum queues only.
# 1. Declare the DLX and dead letter queue
channel.exchange_declare('dlx', exchange_type='direct')
channel.queue_declare('dead-letters', durable=True)
channel.queue_bind('dead-letters', 'dlx', routing_key='failed')
# 2. Configure the main queue to use the DLX (via queue arguments)
# Note: prefer policies over x-arguments in production — policies can
# be updated without deleting/recreating the queue.
channel.queue_declare('orders', durable=True, arguments={
'x-dead-letter-exchange': 'dlx',
'x-dead-letter-routing-key': 'failed', # omit to reuse original routing key
'x-message-ttl': 30000, # 30s TTL
})
Related Topics
Helm Charts
Chart structure, templating engine, release lifecycle, and everything you need to package Kubernetes applications.
Kustomize
Base/overlay architecture, patches, generators, and template-free configuration customization for Kubernetes.
GCP IAM
Identity & Access Management — principals, roles, hierarchy, service accounts, and policy evaluation in one mental model.