Classic Queues Support Priorities
Pre-requisites
This guide assumes familiarity with the essentials of RabbitMQ:
- Tutorials
- The main guide on Queues
- Consumers guide
Please consult the above guides.
What is a Priority Queue
Classic and quorum queues in RabbitMQ support priorities. The standard mode of operation of queues is FIFO (First In, First Out). This means that, ignoring prefetch, competing consumers, requeueing and redeliveries for a moment, RabbitMQ will deliver messages to consumers in the same order the messages were enqueued in.
This standard behavior changes for the queues that are configured to use priorities. For brevity, such queues, both classic and quorum ones, are called "priority queues" in this guide and the rest of RabbitMQ documentation.
Priority queues deliver messages in the order of message priorities. A message priority is a positive integer value set by publishers at publishing time.
Consider a queue with three messages, A, B and C, published with equal priorities and enqueued in that order into a "regular" quorum queue:
| Message | Enqueueing Order | Priority |
|---|---|---|
| A | 1 | 1 |
| B | 2 | 1 |
| C | 3 | 1 |
These messages will be dispatched (sent) to a consumer (or multiple consumers) in the following order: A, B, C.
Now, consider a priority queue with the same messages, but with different priorities:
| Message | Enqueueing Order | Priority |
|---|---|---|
| A | 1 | 1 |
| B | 2 | 3 |
| C | 3 | 2 |
Unlike the standard FIFO delivery behavior, these messages will be dispatched to a consumer (or multiple consumers) in a different order: B, C, A, according to their priorities.
Delivery order can slightly vary in practice when a priority queue has competing consumers, requeued deliveries, or automatic requeueing in case of consumer connection loss or consumer application failure.
Before Adopting Priority Queues: Consider the Alternatives
Priority queue behavior with respect to consumer delivery is much harder to reason about than the standard FIFO behavior of queues, in particular in environments where consumers can often requeue deliveries. For this reason, it's important to consider whether a simpler alternative would be more appropriate.
Priority queues are usually adopted to avoid a classic problem in queueing systems, the head-of-line blocking problem. However, there are several possible alternative solutions that should be considered first:
- Use multiple queues instead of one. Single Giant Queue™ is one of the most common anti-patterns around queue use
- For competing consumers on a single queue, use separate channels with separate prefetch values greater than 1 so that an exhausted prefetch does not block the flow of deliveries
- Using a stream instead of a queue. Streams offer a different consumption pattern and support repeated consumption
- In a limited number of scenarios, a consumer priority can be easier to reason about compared to a priority queue
For example, a set of three queues, priority.low, priority.medium, and priority.high can avoid the head-of-line blocking problem
while keeping the standard delivery behavior, and offer better runtime parallelism as a positive side effect.
Declaration and Supported Priority Ranges
Classic queues support priorities in the [0, 255] range. Quorum queues support a much smaller range: 1 through 4.
For this reason and others, using from 2 to 4 priorities is highly recommended for classic queues (a single priority does not make much practical sense).
Specifically in the case of classic queues, higher priority values will use more CPU and memory resources: RabbitMQ needs to internally maintain a sub-queue for each priority from 1, up to the maximum value configured for a given queue.
A queue can become a priority queue by using client-provided optional arguments.
Declaring a queue as a priority queue using policies is not supported by design. For the reasons why, refer to Why Policy Definition is not Supported for Priority Queues.
Using Client-provided Optional Arguments
To declare a priority queue, use the x-max-priority optional queue argument.
This argument should be a positive integer in the [1, 255] range.
However, as explained above, using from 2 to 4 priorities is highly recommended.
For example, using the Java client:
Channel ch = ...;
Map<String, Object> args = new HashMap<String, Object>();
// Recommendation: use values from 2 to 4 for the maximum number of priorities
args.put("x-max-priority", 4);
ch.queueDeclare("my-priority-queue", true, false, false, args);
Publishers can then publish prioritised messages using the
priority field of
basic.properties. Larger numbers indicate higher
priority.
Priority Queue Behaviour
The AMQP 0-9-1 spec is a little vague about how priorities are expected to work. It states that all queues MUST support at least 2 priorities, and MAY support up to 10. It does not define how messages without a priority property are treated.
By default, RabbitMQ classic queues do not support priorities. When creating priority queues, a maximum priority can be chosen as you see fit. When choosing a priority value, the following factors need to be considered:
-
There is some in-memory and on-disk cost per priority level per queue. There is also an additional CPU cost, especially when consuming, so you may not wish to create huge numbers of levels.
-
The message
priorityfield is defined as an unsigned byte, that is, its values cannot be outside of the [0, 255] range. -
Messages without a
priorityproperty are treated as if their priority were 0. Messages with a priority which is higher than the queue's maximum are treated as if they were published with the maximum priority.
Maximum Number of Priorities and Resource Usage
For environments that adopt publishing with priorities and priority queues, using from 2 to 4 priorities is highly recommended. If you must go higher than 4, using up to 10 priorities is usually sufficient (keep it to a single digit number).
With classic queues, using more priorities consumes more CPU resources by using more Erlang processes. Runtime scheduling would also be affected.
How Priority Queues Work with Consumers
If a consumer connects to an empty priority queue to which messages are subsequently published, the messages may not spend any time waiting in the priority queue before the consumer accepts these messages (all the messages are accepted immediately). In this scenario, the priority queue does not get any opportunity to prioritise the messages, priority is not needed.
However, in most cases, the previous situation is not the norm, therefore you should use the basic.qos (prefetch)
method in manual acknowledgement mode on your consumers to limit the number of messages that can be out for delivery at any time and allow messages to be prioritised.
basic.qos is a value a consumer sets when connecting to a queue. It indicates how many messages the consumer can handle at one time.
The following example attempts to explain how consumers work with priority queues in more detail and also to highlight that sometimes when priority queues work with consumers, higher prioritised messages may in practice need to wait for lower priority messages to be processed first.
Example
-
A new consumer connects to an empty classic (non-prioritised) queue with a consumer prefetch (
basic.qos) of 10. -
A message is published and immediately sent to the consumer for processing.
-
5 more messages are then published quickly and sent to the consumer immediately, because, the consumer has only 1 in-flight (unacknowledged) message out of 10 declared as qos (prefetch).
-
Next, 10 more messages are published quickly and sent to the consumer, only 4 out of the 10 messages are sent to the consumer (because the original
basic.qos(consumer prefetch) value of 10 is now full), the remaining 6 messages must wait in the queue (ready messages). -
The consumer now acknowledges 5 messages so now 5 out of the 6 messages waiting above are then sent to the consumer.
Now Add Priorities
-
As in the example above, a consumer connects with a
basic.qos(consumer prefetch) value of 10. -
10 low priority messages are published and immediately sent to the consumer (
basic.qos(consumer prefetch) has now reached its limit) -
A top-priority message is published, but the prefetch is exceeded now so the top-priority message needs to wait for the messages with lower priority to be processed first.
Interaction with Other Features
In general, priority queues have all the features of standard RabbitMQ queues. There are a couple of interactions that developers should be aware of.
Messages which should expire still only expire from the head of the queue. This means that unlike with normal queues, even per-queue TTL can lead to expired lower-priority messages getting stuck behind non-expired higher priority ones. These messages will never be delivered, but they will appear in queue statistics.
Queues which have a max-length set drop messages as usual from the head of the queue to enforce the limit. This means that higher priority messages might be dropped to make way for lower priority ones, which might not be what you would expect.
Why Policy Definition is not Supported for Priority Queues
The most convenient way to define optional arguments for a queue is using policies. Policies are the recommended way to configure TTL, queue length limits, and other optional queue arguments.
However, policies cannot be used to configure priorities because policies are dynamic and can be changed after a queue has been declared. Priority queues can never change the number of priorities they support after queue declaration, so policies would not be a safe option to use.