The most surprising thing about priority queues is that they don’t actually guarantee instant processing for high-priority items.

Imagine a typical message queue system. Messages arrive, get put in a line, and are processed in the order they arrived (First-In, First-Out, or FIFO). Now, let’s say you have a critical system that needs to handle urgent requests before routine ones. This is where priority queues shine. Instead of a single line, we have multiple queues, each representing a different priority level. When a message comes in, it’s placed into the queue corresponding to its priority.

Let’s see this in action with RabbitMQ, a popular message broker. We’ll set up two queues: high_priority and low_priority.

import pika

# Connect to RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# Declare the queues with priority arguments
channel.queue_declare(queue='high_priority', arguments={'x-max-priority': 10})
channel.queue_declare(queue='low_priority', arguments={'x-max-priority': 10})

# Publish a high-priority message
channel.basic_publish(
    exchange='',
    routing_key='high_priority',
    body='This is a high priority message!',
    properties=pika.BasicProperties(priority=9) # Priority 9 (out of 10)
)

# Publish a low-priority message
channel.basic_publish(
    exchange='',
    routing_key='low_priority',
    body='This is a low priority message.',
    properties=pika.BasicProperties(priority=1) # Priority 1
)

print("Messages published.")
connection.close()

When a consumer connects, it needs to be aware of these priorities. A common setup is to have a single consumer that can fetch from multiple queues, or multiple consumers, each dedicated to a priority level. For a unified consumer, the broker typically prioritizes fetching from the higher-priority queues first.

Here’s how a consumer might be configured to acknowledge priorities:

import pika

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

# Tell the consumer to only fetch from the high_priority queue
# The broker will ensure that messages from high_priority are delivered before
# any messages that might be waiting in other queues (if this consumer were
# configured to listen to multiple queues, which it isn't here for clarity).
channel.basic_consume(
    queue='high_priority',
    on_message_callback=lambda ch, method, properties, body: print(f" [HIGH] Received: {body.decode()}"),
    auto_ack=True
)

# In a real-world scenario, you might have a separate consumer for low_priority
# or a more sophisticated consumer that can handle multiple queues with
# appropriate prefetch counts to balance load and priority.

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

The core idea is that the broker, when asked to deliver a message, will look at all queues that are ready to deliver. If a high-priority queue has messages and a low-priority queue also has messages, the broker will prefer to send a message from the high-priority queue. This preference is what gives you the "priority" behavior. It’s not about a message being magically pulled out of a queue the instant it arrives; it’s about the broker’s delivery mechanism favoring queues with higher-priority waiting messages.

What most people don’t realize is how the broker’s internal mechanisms work to achieve this. For instance, in RabbitMQ, when a consumer acknowledges a message, the broker doesn’t immediately look for the next message of the same priority. Instead, it checks all its queues that are ready to deliver and picks one based on its internal priority logic. This means a lower-priority message can be delivered if no higher-priority messages are available, or if the broker’s internal state leads it to select a lower-priority queue for delivery at that exact moment. The x-max-priority argument on the queue declaration is crucial; it tells the broker that this queue supports priority ordering, and messages published to it should include a priority property.

The real complexity comes when you have many consumers and a mix of high and low priority messages. You need to tune prefetch counts (basic_qos) carefully. If a consumer prefetches too many low-priority messages, it might get "stuck" processing them, starving high-priority messages that arrive later. A common pattern is to have separate consumers for different priority levels, with higher-priority consumers having a smaller prefetch count to ensure they can quickly acknowledge and clear out urgent messages.

The next step after implementing basic priority queues is often managing dead-lettering for messages that can’t be processed, ensuring that even high-priority failures are captured.

Want structured learning?

Take the full Amqp course →