I’ve worked in the integration space for a long time, and messaging has been at the center of it for most of that time. Queues, topics, publish/subscribe, event routing, streaming: the patterns stay, while the products and their names keep changing. The first question I ask in a design review hasn’t changed either. What is actually on the wire, and what does the sender expect to happen next? When teams ask me to compare Azure messaging services, I answer with that question before I name a single product.
In July I wrote Azure Messaging and Orchestration for Integration Architects as part of the Azure PaaS for Integration Architects series. That post drew one line through messaging: “If losing a message would be a business incident, it belongs on Service Bus. If losing it would just mean a missed notification, Event Grid is fine.” I still stand by it. But it deliberately covered only two of the Azure messaging services, and there are five options for moving data between systems: Storage Queues, Service Bus queues, Service Bus topics, Event Grid and Event Hubs.
Recently I saw a LinkedIn post with an infographic listing all five, and it’s a good moment for me to paint a clearer picture. Cheat sheets like that one usually sort by product. After years of fixing integrations, I look at the payload first. This post makes that distinction explicit, gives a clear rule for when to use what, and backs it with one order flow that uses all five services. You can deploy it yourself from github.com/steefjan1/messaging-choices-azure.

Messages, events and streams
Microsoft Learn’s comparison of the messaging services puts the distinction into two definitions:
- An event is “a lightweight notification of a condition or state change. The publisher has no expectation about how the event is handled.”
- A message is “raw data produced by a service to be consumed or stored elsewhere.” And: “A contract exists between publisher and consumer.”
Events split once more. A discrete event reports a state change you can act on. An event series is part of a time-ordered stream that you analyze. One reading from a delivery van tells you nothing; ten thousand of them tell you which van is overheating.
That gives three kinds of payload, and each maps to a family of Azure messaging services. Throughput, cost and the technology stack all matter, but they come second.
Azure messaging services: when to use what
Here’s the short version I give teams, one rule per option.
| Use | When | Not when |
|---|---|---|
| Storage Queue | You have background work that must get done eventually, and losing a message costs you a rerun, not a customer. Reports, thumbnails, file jobs. | You need dead-lettering, ordering, duplicate detection or transactions. You’d end up building them yourself. |
| Service Bus queue | You’re sending a command with exactly one owner, and the business depends on it: ShipOrder, IssueRefund, PostInvoice. | Nobody would notice the loss, or several teams need the same message. |
| Service Bus topic | A business fact has several parties that each owe it something: OrderPlaced goes to payment, inventory and email, each with its own copy, retries and dead-letter queue. | The subscribers owe nothing and just want to know. That’s an event. |
| Event Grid | Something happened, and whoever cares may react: a blob landed, a resource changed, a domain event fired. The publisher doesn’t know or care who listens. | The handler must process it or the business breaks. Put a queue behind Event Grid for that. |
| Event Hubs | You ingest a continuous stream (telemetry, clickstream, logs) where the value is in the aggregate and several readers need the same data. | You need a retry or an acknowledgment per message. A stream has neither. |
Two quick tests settle most debates. Would losing one of these be a business incident? Then it’s Service Bus, as in the July post. Would anyone notice a single missing item? If not, and there are thousands per second, it’s a stream.
Why the rules fall where they do
The table is the short version. The reasons come from how each service behaves when something fails.
Storage Queue or Service Bus
Storage Queue is cheap, it’s HTTP, and it scales to a backlog larger than 80 GB. Learn’s side-by-side comparison is blunt about what you give up: messages up to 64 KB, no ordering guarantee, no duplicate detection, no transactions and no dead-letter queue. To find poison messages, “the application examines the DequeueCount property” and moves them itself. If you use Azure Functions, the host does that for you and fills a -poison queue. That’s a Functions feature, though, not a Storage one.
Service Bus is built for work the business depends on. The broker owns a dead-letter queue per entity and records a reason on each message. You get sessions for FIFO per key, duplicate detection on MessageId, and transactions. Standard tier caps messages at 256 KB; Premium goes to 100 MB and gives you isolated capacity. One tier catch: Basic supports queues only, with no topics, sessions or duplicate detection.
Queue or topic comes down to ownership: a command has one owner, a business fact has several. OrderPlaced sounds like an event, which is why people reach for Event Grid. By Learn’s definition it isn’t one. The publisher very much expects someone to take the money. That expectation is a contract, and a contract belongs on Service Bus.
Event Grid routes, it doesn’t queue
Event Grid pushes to Functions, webhooks, Logic Apps and queues, and namespace topics add pull delivery and an MQTT broker. Its delivery behavior tells you what it’s built for. The default retry policy is 30 attempts within 1,440 minutes, backing off from 10 seconds to 12 hours. “Event Grid doesn’t guarantee order for event delivery.” And “by default, Event Grid doesn’t turn on dead-lettering”: an event that runs out of retries is simply dropped unless you configured a storage container for it.
That’s fine for a notification, not for work. So the pattern that holds up in production puts a queue behind Event Grid: the event says “a blob arrived,” and a queue holds the job of processing it, with its own pace, retries and poison handling.
Event Hubs is a log, not a queue
Event Hubs is the one service in the set where reading a message doesn’t remove it. It’s a partitioned, append-only log. Consumers track their own position with checkpoints, and “checkpointing is the consumer’s responsibility.” Events stay until the retention period expires: up to 7 days on Standard and 90 on Premium and Dedicated.
Three things follow from that. Ordering holds only within a partition, so you pick a partition key (the van id, the device id) to keep related events in order. Consumer groups give independent readers of the same stream, each with its own checkpoint. And there’s no per-message acknowledgment and no dead-letter queue. A consumer that can’t handle event 4,711 has to decide for itself what to do and then move on. That’s why it’s excellent for telemetry and wrong for order processing, however high the volume.
One order flow, all five services
The companion repo puts this into one e-commerce flow. It runs on a single Azure Functions app (.NET 8 isolated, Flex Consumption) and deploys with azd up to Sweden Central. All connections use the app’s managed identity, and local auth is disabled on Service Bus, Event Hubs and both storage accounts.

Orders: messages on Service Bus
POST /api/orders publishes OrderPlaced to a Service Bus topic with four subscriptions. The fraud-review subscription uses a SQL rule, total >= 1000, so it only sees expensive orders. Rules evaluate application properties, not the body, and that’s why the publisher sets them explicitly:
var message = new ServiceBusMessage(BinaryData.FromObjectAsJson(order, Json)){ MessageId = order.OrderId, // duplicate detection keys on this Subject = nameof(OrderPlaced),};message.ApplicationProperties["total"] = (double)order.Total; // the SQL rule reads this
The payment handler settles its own messages. It dead-letters an order it can never charge with reason InvalidTotal. If it hits a failure that looks transient, it throws and lets the broker retry, and after three deliveries the broker dead-letters the message with MaxDeliveryCountExceeded. On success, it sends a ShipOrder command to a session-enabled queue with SessionId = customerId.
One detail is easy to miss: sending the command and completing the incoming message are two operations, not one transaction. After a crash between them, the order is redelivered and the command sent again. Duplicate detection on shipments drops the second copy, because the command’s MessageId is derived from the order id. At-least-once delivery means idempotent handlers, and broker features like this take some of that work off your hands.
Product images: an event, then work
A blob upload to product-images raises BlobCreated on an Event Grid system topic. The subscription is set to 30 attempts within 24 hours and has dead-lettering turned on, since it’s off by default. OnImageUploaded doesn’t process the image. It drops an ImageJob on a Storage queue and returns. ImageWorker does the work, and anything that fails three dequeues lands in image-jobs-poison. The uploader knows nothing about any of this, which is the point of an event.
Telemetry: a stream on Event Hubs
POST /api/telemetry simulates delivery vans and sends batches to an event hub with four partitions, keyed by van id. Two functions read the same events through two consumer groups: one computes averages, the other flags overheating engines. Take the alerts function down for an hour and it resumes from its checkpoint, and the aggregator never notices.
What the demo run showed
scripts/demo.ps1 drives every scenario, including a duplicate order, a poison order and a corrupt image, and then peeks at where the failures ended up. On my run against Sweden Central, the output looked like this:
| Where | Message | What the platform recorded |
|---|---|---|
orders/payment dead-letter queue | zero-total order | reason InvalidTotal, description “Order total 0 is not payable.”, delivery count 0 |
orders/payment dead-letter queue | poison customer | reason MaxDeliveryCountExceeded, “could not be consumed after 3 delivery attempts”, delivery count 3 |
shipments dead-letter queue | none | empty: every paid order shipped |
image-jobs-poison Storage queue | corrupt-104734.png | the job body and a dequeue count of 0 |
That table is the lesson in one picture, and it’s where Azure messaging services differ most. The Service Bus dead-letter queue belongs to the broker, and every entry tells you why it’s there and how often it was tried. The dead-lettered order also never burned a retry, because the handler knew it could never succeed. The Storage poison queue only exists because the Functions host moved the message there, and it arrives with no reason and a dequeue count reset to 0. The history of three failed attempts is gone. If a Storage queue carries work you’ll have to explain in an incident review, you’ll be rebuilding that history yourself.

The publisher also can’t tell a duplicate from a first send. Both POSTs of the same order came back accepted, and the API logged “published” twice. The broker drops the second copy silently, inside its 10-minute duplicate detection window.
Where this framing is the wrong answer
No rule for picking Azure messaging services survives every context. These are the ones I’ve seen bend it.
- When the volume argument is real. A few thousand orders per second is still a message workload. Before you move it to Event Hubs and rebuild dead-lettering yourself, look at Service Bus Premium with more messaging units.
- When the devices need identity or commands back. Real vans don’t write straight to Event Hubs. If you need per-device authentication or cloud-to-device messages, look at IoT Hub or the Event Grid MQTT broker in front of the stream.
- When you don’t need a broker at all. A synchronous call with a retry policy is simpler than a queue with one producer and one consumer in the same deployment. Add a queue when you need to decouple, not by default.
- When the event crosses an organizational boundary. Service Bus subscriptions are cheap inside one team. Across teams or companies, Event Grid with CloudEvents and filtered subscriptions is usually the looser and better contract.
- When cost dominates. Event Hubs Standard bills per throughput unit whether you send anything or not, and Service Bus Standard has a base charge. Run
azd down --purgeafter the demo.
Takeaway
Choosing between Azure messaging services doesn’t start with “which Azure service?” It starts with “what am I sending, and what does the sender expect?” A command goes to a Service Bus queue, a business fact with several owners to a topic, and cheap background work to a Storage queue. A notification without expectations goes to Event Grid, with a queue behind it for the actual work. A stream goes to Event Hubs. As I wrote in July, the craft isn’t picking a winner. Most real systems use several of these, and the interesting design work happens at the seams between them.
The repo, diagrams and demo script are at github.com/steefjan1/messaging-choices-azure.
Sources
- Compare Azure messaging services, Microsoft Learn
- Storage queues and Service Bus queues: compared and contrasted, Microsoft Learn
- Service Bus Premium and Standard tiers, Microsoft Learn
- Event Grid message delivery and retry, Microsoft Learn
- Event Grid overview, Microsoft Learn
- Event Hubs features and terminology, Microsoft Learn

















