The problem
Most queues hand out work in arrival order. That is fair, but it ignores what each message is for. When a nightly batch puts hundreds of routine tasks on the same queue as interactive work, a password-reset email or a "payment failed" notice waits behind all of them, and someone at a screen waits with it. More consumers shorten the wait on average, yet every urgent message still sits behind the whole backlog that arrived before it.
How it works
Give each message a priority when it is produced, and let the consumers take higher-priority work first. There are two designs:
- One queue with broker-side priorities. The broker keeps the queue ordered by a priority field and hands out the most important message first. RabbitMQ does this. Classic queues need the
x-max-priorityargument when the queue is declared (1 to 255; the documentation recommends only a handful of levels, and a policy can't set it). Quorum queues always support priorities: strictly, with levels 0 to 31, since RabbitMQ 4.3; versions 4.0 to 4.2 had two levels, normal and high, delivered two high for every normal. - A queue, or a subscription, per priority. Producers send each message to the high or the low queue, or publish to one topic and let a subscription per priority filter on an attribute. This works on brokers that have no priority field: Amazon SQS (AWS suggests separate queues for prioritization; its fair queues balance tenants, not urgency), Azure Service Bus (the Azure pattern's example sets a custom
Priorityproperty and routes it with SQL filters to a high and a low subscription), Google Cloud Pub/Sub (a message carries attributes but no priority, and subscription filters can match an attribute) and Apache Kafka (a topic per priority).
The consumers then choose what to take next:
- High-first polling. One pool checks the high queue and takes low work only when high is empty. It gives urgent work the shortest wait and wastes no capacity, but low work starves whenever high never empties.
- A dedicated pool per queue. Each queue has its own consumers and its own scaling limits; the Azure sample lets the high consumer scale out to 200 instances and the low one to 40. Classes are isolated, but idle high consumers don't help with a low backlog unless you let them.
- Weighted ratios. The pool takes, for example, three high messages for every low one. Low work always moves, at the cost of some urgent latency.
In the diagram, four workers each finish 2.5 jobs a second (10 in total, 0.4 s per job). Behind 500 routine jobs in one queue, the reset email waits 500 ÷ 10 = 50 s; with a high queue it waits only for the next free worker.
When to use it
- Interactive work shares workers with batch work: password resets and order confirmations next to imports, re-indexing or report generation.
- Customers or tenants buy different service levels, and premium requests need a tighter latency target.
- Alerts and pages must not wait behind routine reports or digests.
- Not when all work is equally urgent, or when only average throughput matters: priority adds queues, routing rules and failure modes without improving either.
Trade-offs
- Starvation. Strict high-first service can hold low work back for as long as urgent work keeps coming. Bound it: reserve consumers for low (one of four workers in the diagram, so high gets 7.5 jobs a second and low 2.5), pull in a fixed ratio, or age messages by promoting anything that has waited past its target to the higher queue. RabbitMQ's documentation notes that classic queues serve their priority levels in cycles so lower levels are never starved, while strict quorum queues can delay them indefinitely.
- No pre-emption. Priority only chooses the next message. A long low-priority job that has already started keeps its worker until it finishes, so split long jobs into short steps with checkpoints and the workers come back to the queue often.
- Order across priorities is lost. A later urgent message overtakes earlier routine ones. If two messages change the same entity, keep them in the same class or make the consumer cope with any order.
- Priority is not capacity. If urgent work alone exceeds what the consumers finish, the high queue grows too. Scale each queue's consumers on its own depth, or on the age of its oldest message: see Autoscaling and Competing Consumers.
- More moving parts. Separate queues bring separate dead-letter queues, alarms, permissions and polling costs.
Implementation notes
- Priority only matters when work waits. With RabbitMQ, use manual acknowledgements and a small prefetch: a consumer with a large prefetch has already taken low-priority messages before the urgent one arrives. With SQS, long polling an empty high queue holds the loop for up to the wait time before it looks at the low queue; use a short wait in a high-first loop, or separate pollers.
- Kafka has no message priority. A consumer fetches from all of its partitions at once; to favour a high-priority topic, pause the low topic's partitions while high has a backlog and resume them afterwards, or run a separate consumer group per topic.
- Decide who sets priority. Derive it from facts the system trusts (the message type, the customer's plan), not from a flag any producer can set. Restrict who may publish to the high queue with separate credentials or topic permissions, and watch the volume per producer: if every producer marks its messages urgent, nothing is.
- Scale each queue on its own signal. KEDA has scalers for RabbitMQ, Amazon SQS, Azure Service Bus, Google Cloud Pub/Sub and Kafka; the Pub/Sub scaler can scale on the age of the oldest unacknowledged message, and SQS publishes a matching signal as the CloudWatch metric
ApproximateAgeOfOldestMessage. - Dead-letter per queue. Give each priority its own dead-letter queue, so a poison urgent message can't block the high path and its alarm reaches the right team.
- Monitor per class. Track depth and the age of the oldest message for each queue against a target per class, for example a few seconds for high and an hour for low. An average across classes hides starvation.
- Rank work at the edge too. Queues are one place to prioritise. At the API, shed the least critical requests first when overloaded (Rate Limiting; Google's SRE book describes criticality levels that travel with each request), and give each class its own pool so one can't exhaust another (Bulkhead).