RabbitMQ mirroring is less about redundancy and more about ensuring that your consumers don’t suddenly find themselves with no messages to process.

Let’s see this in action. Imagine a simple scenario: a publisher sending messages to a queue, and a consumer reading from it.

# Publisher
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='my_mirrored_queue', durable=True)
channel.basic_publish(exchange='', routing_key='my_mirrored_queue', body='Hello, mirrored world!')
connection.close()

# Consumer
import pika

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

def callback(ch, method, properties, body):
    print(f" [x] Received {body.decode()}")

channel.basic_consume(queue='my_mirrored_queue', on_message_callback=callback, auto_ack=True)
channel.start_consuming()

Now, if we run this publisher and consumer, and then stop the RabbitMQ node running my_mirrored_queue, the consumer will immediately stop receiving messages. The queue effectively vanishes because it was only on that single node. This is where mirroring comes in.

Mirroring allows you to create copies (replicas) of a queue across multiple RabbitMQ nodes in a cluster. When you declare a mirrored queue, you specify which nodes should host a replica. One of these replicas becomes the "master" or "primary" queue, responsible for receiving all incoming messages and acknowledgments. The other replicas are "mirrors" and passively receive updates from the master. If the master node fails, one of the mirrors is automatically promoted to become the new master, ensuring that message delivery can continue with minimal interruption.

Here’s how you configure a mirrored queue. Let’s assume you have a three-node cluster: rabbit1, rabbit2, and rabbit3. You’d typically define the policy for mirroring using RabbitMQ’s management UI or via the command line.

First, ensure your nodes are clustered. You can check this with rabbitmqctl cluster_status. If they aren’t, you’d typically stop the RabbitMQ service on all nodes and then run:

On rabbit2:

rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@rabbit1
rabbitmqctl start_app

On rabbit3:

rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@rabbit1
rabbitmqctl start_app

Once clustered, you define a policy. A policy is a set of rules that RabbitMQ applies to queues or exchanges. To create a mirrored queue policy, you’d use rabbitmqctl set_policy.

Let’s say you want all queues matching the pattern mirrored-.* to be mirrored across all nodes in the cluster. The command would look like this:

rabbitmqctl set_policy \
  --priority 0 \
  --apply-to queues \
  "my_mirror_policy" \
  "^mirrored-.*" \
  '{"ha-mode": "all", "ha-sync-mode": "automatic"}'

Let’s break this down:

  • --priority 0: This policy has the highest priority (lower number means higher priority).
  • --apply-to queues: This policy applies only to queues. You could also apply it to exchanges.
  • "my_mirror_policy": This is the name of your policy.
  • "^mirrored-.*": This is a regular expression that matches queue names starting with mirrored-. Any queue declared with a name like mirrored-orders or mirrored-logs will be subject to this policy.
  • '{"ha-mode": "all", "ha-sync-mode": "automatic"}': This is the core of the mirroring configuration.
    • "ha-mode": "all": This instructs RabbitMQ to mirror the queue to all nodes in the cluster. Other options include "exactly" (where you specify a number of replicas) or "nodes" (where you list specific nodes).
    • "ha-sync-mode": "automatic": This means that when a new mirror is added or a node rejoins the cluster, it will automatically synchronize its state with the master. The alternative is "manual", which requires explicit commands to sync.

After applying this policy, if you declare a queue named mirrored-tasks (e.g., using channel.queue_declare(queue='mirrored-tasks', durable=True)), RabbitMQ will automatically create replicas of this queue on all nodes in the cluster. The first node that the queue is declared on will typically become the master.

If the master node goes down, RabbitMQ will elect a new master from the remaining mirrors. Consumers connected to the cluster will automatically reconnect to the new master. Publishers sending messages to the queue will continue to do so without interruption, provided they are using a connection that can handle reconnections (e.g., using pika.BlockingConnection with a connection_attempts parameter or pika.SelectConnection).

The surprising part is that even with ha-mode: "all", the performance overhead isn’t always as bad as you’d expect for writes. RabbitMQ uses a clever approach where the master queue handles all the incoming traffic, and then asynchronously replicates the messages to the mirrors. This means publishers don’t have to wait for all mirrors to acknowledge receipt of a message, only the master. The actual bottleneck often becomes the network latency between nodes and the disk I/O on the master node.

The next hurdle you’ll likely encounter is managing queue synchronization when nodes are added or removed, especially if you’re not using ha-sync-mode: "automatic" or if network partitions occur.

Want structured learning?

Take the full Amqp course →