RabbitMQ doesn’t actually guarantee message ordering across multiple consumers.

Let’s see this in action. Imagine we have a simple RabbitMQ setup: a single queue named my_ordered_queue and a direct exchange named my_exchange. We’ll publish messages to this exchange, which then route to our queue.

Here’s what happens when we publish messages:

import pika

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

channel.exchange_declare(exchange='my_exchange', exchange_type='direct')
channel.queue_declare(queue='my_ordered_queue', durable=True)
channel.queue_bind(exchange='my_exchange', queue='my_ordered_queue', routing_key='order_key')

for i in range(5):
    message_body = f"Message {i}"
    channel.basic_publish(
        exchange='my_exchange',
        routing_key='order_key',
        body=message_body,
        properties=pika.BasicProperties(
            delivery_mode=2,  # Make message persistent
        ))
    print(f" [x] Sent '{message_body}'")

connection.close()

Now, let’s set up two consumers to process these messages concurrently.

Consumer 1:

import pika
import time

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

channel.queue_declare(queue='my_ordered_queue', durable=True)

def callback1(ch, method, properties, body):
    print(f" [x] Consumer 1 received {body.decode()}")
    time.sleep(1) # Simulate work
    ch.basic_ack(delivery_tag=method.delivery_tag)

channel.basic_qos(prefetch_count=1) # Process one message at a time
channel.basic_consume(queue='my_ordered_queue', on_message_callback=callback1)

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

Consumer 2:

import pika
import time

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

channel.queue_declare(queue='my_ordered_queue', durable=True)

def callback2(ch, method, properties, body):
    print(f" [y] Consumer 2 received {body.decode()}")
    time.sleep(1.5) # Simulate slightly longer work
    ch.basic_ack(delivery_tag=method.delivery_tag)

channel.basic_qos(prefetch_count=1) # Process one message at a time
channel.basic_consume(queue='my_ordered_queue', on_message_callback=callback2)

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

If you run both consumers, you’ll likely see output like this (order may vary slightly):

 [*] Waiting for messages. To exit press CTRL+C
 [x] Consumer 1 received Message 0
 [*] Waiting for messages. To exit press CTRL+C
 [y] Consumer 2 received Message 1
 [x] Consumer 1 received Message 2
 [y] Consumer 2 received Message 3
 [x] Consumer 1 received Message 4

Notice how Message 0 was received by Consumer 1, but Message 1 was received by Consumer 2, even though Message 0 was sent first. This happens because RabbitMQ’s default behavior is to distribute messages to available consumers. Once a message is delivered to a consumer and not acknowledged, it’s not delivered to another. However, if multiple messages are in the queue, and multiple consumers are active, RabbitMQ will deliver them to whichever consumer is ready to process them. The basic_qos(prefetch_count=1) ensures a consumer only holds one unacknowledged message at a time, but it doesn’t prevent other consumers from picking up subsequent messages.

The core problem is that the queue itself doesn’t intrinsically enforce a strict, single-threaded processing order when multiple consumers are attached. RabbitMQ’s primary goal is reliable message delivery, not necessarily strict sequential processing across a distributed system.

To achieve reliable ordering, you need to ensure that only a single consumer ever processes messages from a given queue. This is often achieved by:

  1. Single Consumer per Queue: The most straightforward approach. Have one process, or one worker thread within a process, consuming from the queue. If you need to scale, you scale by adding more queues, not by adding more consumers to a single queue.

    • Diagnosis: Observe your RabbitMQ management UI. If a queue has multiple consumers listed, you are not guaranteed strict ordering.
    • Fix: Reconfigure your deployment to ensure only one basic_consume call is active for any given queue. This might involve using a single instance of your consumer application or careful process management.
    • Why it works: With only one consumer, it must process messages in the order they arrive in the queue, as it’s the only entity capable of dequeuing them.
  2. Per-Message Ordering Key (with Fanout/Topic and Separate Queues): For scenarios where you need ordering within a specific context (e.g., all messages for user_id=123 must be ordered, but messages for user_id=456 can be processed in parallel), you can use a fanout or topic exchange with a routing key that includes the ordering context.

    • Diagnosis: You’ll see messages for the same ordering key being processed out of order by different consumers if multiple queues are involved.
    • Fix: For each unique ordering key (e.g., user_id), create a dedicated queue and bind it to the exchange with that specific routing key. Then, assign a single consumer to each of these dedicated queues.
    • Why it works: Each unique ordering key gets its own queue, and each of those queues has only one consumer. This creates isolated, ordered processing streams for each key.
  3. Client-Side Reordering (Less Common/More Complex): The consumer receives messages and buffers them, then reorders them based on a sequence number embedded in the message properties or body. This is complex and often defeats the purpose of using a message queue for ordered processing.

    • Diagnosis: The application logic itself is out of order, despite messages arriving sequentially in the queue.
    • Fix: Implement a robust buffering and reordering mechanism within the consumer. This typically involves storing messages in memory or a temporary store until the expected sequence number arrives, then processing them in order.
    • Why it works: The consumer takes responsibility for the ordering that RabbitMQ doesn’t provide natively in a multi-consumer setup.
  4. Message Prioritization (Not for Ordering): RabbitMQ supports message priorities, but this is for ensuring higher-priority messages are delivered before lower-priority ones, not for strict sequential order of messages with the same priority.

    • Diagnosis: Messages with the same priority are being processed out of order.
    • Fix: Message priorities are not a solution for strict ordering. Re-evaluate your design to use single consumers per queue or per ordering key.
    • Why it works: This is a "why it doesn’t work" point. Priorities are about urgency, not sequence.
  5. Publisher Confirms and basic_nack with Requeue: While not directly for ordering, understanding these helps with reliability. Publisher confirms ensure messages reach the broker. basic_nack with requeue=True can return a message to the queue if processing fails, but if multiple consumers are active, it doesn’t guarantee it will be re-delivered to the same consumer or processed next.

    • Diagnosis: Messages are lost or duplicated due to consumer failures, and ordering is further disrupted.
    • Fix: Implement publisher confirms and use basic_ack when processing is complete. For transient failures where re-processing is desired, use basic_nack(delivery_tag=method.delivery_tag, requeue=True) only if you have a single consumer. If multiple consumers exist, requeue=True can lead to infinite loops or unpredictable delivery.
    • Why it works: Confirms guarantee delivery to the broker. nack with requeue allows retries, but its effectiveness for ordering is limited by the consumer count.

The common thread is that if you need strict, guaranteed message ordering, you must limit the number of consumers processing messages from a given queue to one. If you need to scale, you achieve that by partitioning your work into multiple queues, each with its own single consumer.

The next problem you’ll encounter is how to manage state across these single-consumer, partitioned queues when the "state" relates to a broader entity than what a single queue can represent.

Want structured learning?

Take the full Rabbitmq course →