Message ordering
A broker keeps messages in order within one queue, partition or session, and only while a single consumer handles them one at a time. Across a whole system there is no order. A consumer can rely on the order of events for one order ID, and on nothing about how those events interleave with events for another order.
How each broker gives order
| Broker | Ordered unit | How | Where it breaks |
|---|---|---|---|
| Kafka | A partition | Records with the same key go to the same partition, and a consumer reads a partition in the order written | A partition holds many keys, so there is no order between keys, and no order across partitions |
| Azure Service Bus | A session | Messages share a SessionId; one receiver holds the session lock and gets them in order | Sessions must be enabled (Standard or Premium tier). A sequence number alone orders the queue, not the processing |
| RabbitMQ | A queue with one consumer | A queue delivers in publish order | Several consumers on one queue, or a requeued message, change the order |
Competing consumers lose it
Two consumers share a queue holding messages 1, 2 and 3. Consumer 1 takes message 1 and is slow. Consumer 2 takes messages 2 and 3 and finishes both. The messages were handled in the order 2, 3, 1. More consumers on one queue means more throughput and less order. The way to keep both is to give each key its own lane: a Kafka key, or a Service Bus session, so that all messages for order 42 reach the same consumer in sequence while other orders go elsewhere. See partitions and consumer groupsPartitions, offsets and consumer groupsHow a stream splits a topic into lanes, marks each reader's place, and shares the work among a group.Opens in a new tab and queues and topicsQueue vs topicA queue gives each message to one consumer, a topic gives every subscriber its own copy.Opens in a new tab.
A message that fails and is retried later also arrives after the ones behind it (see retries and backoffRetries and backoffRun a failed message again after a growing, randomised delay, and stop after a limit.Opens in a new tab).
Version numbers
Where order cannot be guaranteed, make it harmless. Each event carries the version of the entity it describes: OrderPlaced is version 1, PaymentReceived is 2, OrderShipped is 3. The consumer stores the highest version it has applied and ignores any message that is not newer.
var rows = await db.OrderViews .Where(v => v.OrderId == msg.OrderId && v.Version < msg.Version) .ExecuteUpdateAsync(s => s .SetProperty(v => v.Status, msg.Status) .SetProperty(v => v.Version, msg.Version), ct); if (rows == 0){ // The view already holds this version or a newer one, so this message is stale. logger.LogInformation("Ignored stale version {Version} of order {OrderId}", msg.Version, msg.OrderId);}The condition on line 2 makes the check and the update one statement, so two consumers cannot interleave between them. To also detect a gap, such as version 3 arriving when the stored version is 1, require v.Version == msg.Version - 1 instead and hold back anything else. What to do with a held-back message after a failure is the subject of dead letters and ordered messagesDead letters and ordered messagesA dead-lettered message leaves the messages after it running on state that never happened.Opens in a new tab.
Sources
- Introduction to Apache Kafka, Apache Kafka documentation · accessed June 12, 2026
- Enable FIFO with sessions, Azure Service Bus, Microsoft Learn · accessed June 12, 2026
- Consumer Acknowledgements and Publisher Confirms, RabbitMQ · accessed June 12, 2026
- Idempotent Consumer pattern, Azure Architecture Center · accessed June 12, 2026