Introduction to RabbitMQ

RabbitMQ is an open-source message broker that implements the AMQP 0-9-1 protocol. Written in Erlang and maintained by Broadcom, it decouples producers from consumers a publisher sends a message without knowing which service will process it, and a consumer processes messages without knowing which service sent them.

RabbitMQ stores messages in queues and delivers them to consumers. It does not process messages itself that is the responsibility of your application code.

Architecture

graph LR
  Producer["Producer\n(publishes)"]
  Exchange["Exchange\n(routes)"]
  Q1["Queue A"]
  Q2["Queue B"]
  Consumer1["Consumer 1"]
  Consumer2["Consumer 2"]

  Producer -->|"message + routing key"| Exchange
  Exchange -->|"binding"| Q1
  Exchange -->|"binding"| Q2
  Q1 -->|"deliver"| Consumer1
  Q2 -->|"deliver"| Consumer2

Messages flow from producers to exchanges. Exchanges inspect the routing key and route messages to one or more bound queues. Consumers subscribe to queues and process messages independently.

Core AMQP concepts

Producer

An application that creates and publishes messages to an exchange. A producer declares which exchange to publish to and attaches a routing key that the exchange uses for routing decisions.

Exchange

The exchange receives messages from producers and routes them to queues based on its type and the message’s routing key. An exchange does not store messages if no queue is bound to it for a given routing key, the message is dropped (or returned to the producer if mandatory is set).

Queue

A named buffer that holds messages until a consumer retrieves them. Queues can be durable (survive broker restart) or transient. Multiple consumers can subscribe to the same queue, in which case RabbitMQ distributes messages round-robin between them.

Binding

A binding is a rule that connects an exchange to a queue. It may include a binding key (for direct and topic exchanges) or header attributes (for headers exchanges) that control which messages are routed to which queue.

Consumer

An application that subscribes to a queue and processes messages. A consumer registers a callback that is invoked when a message arrives. After processing, it sends an acknowledgement to tell RabbitMQ the message was handled.

Channel

A channel is a lightweight virtual connection multiplexed over a single TCP connection. Applications typically open one channel per thread rather than one TCP connection per thread opening TCP connections is expensive; opening channels is cheap.

Exchange types

Type Routing behaviour Routing key used? Typical use case
direct Routes to queues whose binding key exactly matches the routing key Yes Task queues, targeted delivery
fanout Broadcasts to all bound queues, ignores routing key No Pub/sub, event broadcasting
topic Routes by pattern: * matches one word, # matches zero or more words Yes (pattern) Filtered event streams
headers Routes by matching message header attributes instead of routing key No (headers) Attribute-based routing

Topic exchange pattern examples

Routing key Pattern *.error Pattern payments.# Pattern #
app.error match no match
payments.created no match match
payments.order.created no match match
audit.info no no match

Message acknowledgements

By default, RabbitMQ considers a message delivered once it is sent to the consumer. In production you should use manual acknowledgements so a message is only removed from the queue after your code has successfully processed it.

Acknowledgement Meaning
basic.ack Message processed successfully; remove from queue
basic.nack Processing failed; optionally requeue
basic.reject Processing failed for a single message; optionally requeue

If a consumer disconnects without sending an ack, RabbitMQ requeues the message and delivers it to another consumer.

Dead-letter queues

When a message is rejected without requeue, expires (TTL), or exceeds the queue’s maximum length, RabbitMQ can route it to a dead-letter exchange (DLX). Binding a dead-letter queue to the DLX lets you inspect, alert on, or replay failed messages.

1
2
3
4
5
6
7
8
channel.queue_declare(
    queue='orders',
    durable=True,
    arguments={
        'x-dead-letter-exchange': 'orders.dlx',
        'x-message-ttl': 60000,  # 60 seconds
    }
)

Durability and persistence

By default, queues and messages are transient a broker restart loses them. For reliable messaging:

  • Declare queues with durable=True the queue definition survives a restart.
  • Publish messages with delivery_mode=2 (persistent) message bodies are written to disk.
  • Enable publisher confirms the broker acknowledges each message once it has been written to disk, so you know it is safe.

All three are required together. A durable queue with non-persistent messages still loses undelivered messages on restart.

Practical example

Install pika

1
pip install pika

Publish a message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pika

connection = pika.BlockingConnection(
    pika.ConnectionParameters(host='localhost')
)
channel = connection.channel()

channel.exchange_declare(exchange='orders', exchange_type='direct', durable=True)
channel.queue_declare(queue='orders.created', durable=True)
channel.queue_bind(queue='orders.created', exchange='orders', routing_key='created')

channel.basic_publish(
    exchange='orders',
    routing_key='created',
    body=b'{"order_id": 42}',
    properties=pika.BasicProperties(delivery_mode=2),  # persistent
)

print('Message published')
connection.close()

Consume messages

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pika

def on_message(channel, method, properties, body):
    print(f'Received: {body.decode()}')
    channel.basic_ack(delivery_tag=method.delivery_tag)  # manual ack

connection = pika.BlockingConnection(
    pika.ConnectionParameters(host='localhost')
)
channel = connection.channel()

channel.basic_qos(prefetch_count=1)  # process one at a time
channel.basic_consume(queue='orders.created', on_message_callback=on_message)

print('Waiting for messages...')
channel.start_consuming()

basic_qos(prefetch_count=1) prevents RabbitMQ from dispatching more than one unacknowledged message to a consumer at a time, ensuring work is distributed evenly when consumers have different processing speeds.

What to avoid

Do not use auto-ack in production. With auto_ack=True, RabbitMQ marks messages as delivered the moment it sends them. If your consumer crashes mid-processing, the message is lost with no way to recover it.

Do not use fanout when you need targeted routing. Fanout sends every message to every bound queue regardless of content. Use direct or topic exchanges when only specific consumers should receive a message.

Do not skip dead-letter queues. Without a DLX, rejected or expired messages disappear silently. Always configure a dead-letter exchange so you can audit and replay failed messages.

Do not ignore publisher confirms for critical messages. Publishing to RabbitMQ is asynchronous by default. Without confirms, a network blip between your producer and the broker can silently drop messages. Enable channel.confirm_delivery() for financial or audit-critical flows.

Do not share one queue across unrelated consumers. If two unrelated services consume from the same queue, each receives roughly half the messages both will miss messages intended for the other. Give each logical consumer its own queue bound to a shared exchange instead.