Messages, Events or Streams: Choosing Azure Messaging by What You Send

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.

UseWhenNot when
Storage QueueYou 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 queueYou’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 topicA 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 GridSomething 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 HubsYou 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:

WhereMessageWhat the platform recorded
orders/payment dead-letter queuezero-total orderreason InvalidTotal, description “Order total 0 is not payable.”, delivery count 0
orders/payment dead-letter queuepoison customerreason MaxDeliveryCountExceeded, “could not be consumed after 3 delivery attempts”, delivery count 3
shipments dead-letter queuenoneempty: every paid order shipped
image-jobs-poison Storage queuecorrupt-104734.pngthe 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 --purge after 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

Azure Messaging and Orchestration for Integration Architects

In the Azure PaaS map post, the integration layer got a single paragraph. It named four services: Logic Apps, API Management, Service Bus, and Event Grid, and moved on. This post takes Azure messaging and orchestration apart into the decisions underneath that paragraph.

I won’t tour features here. Two of these services already have their own deep series on this blog, so re-covering them would waste your time. Instead, I’ll stay at the decision layer. When do you reach for which? And why do teams so often reach wrong? Those are the questions that actually cost you in production.

Azure messaging and orchestration: two axes decide almost everything

Four services sound like four choices. In practice, though, only two questions matter, and they cut across the whole layer.

  • First: is this messaging or orchestration? Messaging moves events and data between systems. Orchestration coordinates a multi-step process toward an outcome. The two look similar on a whiteboard, but they fail differently, scale differently, and belong to different services. So separate them before anything else.
  • Second: what does failure cost, and what shape is the work? Once you know whether you’re moving messages or coordinating steps, the follow-up question splits the choice further. For messaging, the cost of a lost message decides it. For orchestration, the complexity of the flow and the team who owns it decide it.

Get those two axes clear, and the service almost picks itself. Skip them, and you end up with Event Grid where you needed guarantees, or a Logic App doing work that belonged in code.

Messaging: Service Bus vs Event Grid

Both move things between systems. That’s where the similarity ends.

  • Service Bus is the durable, ordered, transactional backbone. Reach for it when delivery has to be guaranteed. It gives you sessions for ordered processing, dead-lettering for messages that can’t be handled, and transactional handling across multiple operations. Topics and subscriptions add pub/sub without a separate broker. So Service Bus fits business messages: an order, a payment, a claim, where losing one is an incident.
  • Event Grid is a lightweight, high-volume router. It broadcasts events to whoever cares: resource state changes, custom application events, and telemetry. It’s built for throughput and fire-and-forget delivery, not guaranteed processing. Therefore, it fits notifications and reactive triggers, where a missed event is a shrug rather than a page.

Here’s the rule of thumb I give teams new to Azure 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.

And often you use both. A common pattern pairs them: Event Grid fans out a notification, and a subscriber drops a durable message onto Service Bus for guaranteed processing. That way you get Event Grid’s reach and Service Bus’s reliability in one flow, each doing the job it’s good at.

The honest note, though, is that this is where teams get burned. Event Grid looks simpler, so teams default to it. Then, weeks later, they discover the workload actually needed ordering or delivery guarantees. Now they’re bolting reliability onto a service that was never designed for it. So decide on the message-loss cost first, before the “which feels easier” instinct takes over.

Orchestration: Logic Apps vs code

Messaging moves things. Orchestration coordinates them. The decision here isn’t about reliability; it’s about complexity and ownership.

  • Logic Apps is the designer-first route. It shines when you need enterprise connectors SAP, IBM MQ, mainframe hosts, the long tail of line-of-business systems without a modern REST API. Standard Logic Apps also closes the old gaps that made it hard in regulated environments: VNet integration, built-in state, per-workflow scaling. So for a workflow that a less code-heavy team will own and maintain, Logic Apps is often the right call even when a Function would be more elegant.
  • Code is the route once complexity climbs. Designer workflows are fast to build and easy to read at first. Past a certain size, though, they get hard to reason about and harder to code-review. In my experience, the practical ceiling sits around a dozen actions with a couple of branches. Beyond that, do one of two things. Either decompose the workflow into smaller ones, or move the logic into a Function where a proper language and real tests take over.

The deciding questions, then, are simple. Who maintains this: a low-code team or engineers? How complex is the flow really? And can you review it a year from now? For the deeper mechanics of building agentic workflows in Logic Apps, I covered that ground in the Logic Apps Agent Loop series so that I won’t repeat it here.

Where API Management fits

Azure API Management (APIM) isn’t messaging or orchestration. Instead, it’s the control point in front of both.

It sits between consumers and whatever does the real work: a Logic App, a Function, an App Service backend. From there, it enforces rate limits, authentication, transformation, and policy-based routing. So when multiple consumers hit a shared set of backend capabilities, APIM lets you change the implementation behind them without breaking anyone, and lets you enforce policy without touching application code.

That’s all I’ll say here, because APIM earns a series of its own. I went deep on it in the APIM for AI workloads series, including how it behaves as an AI gateway. For this layer, treat it as the front door that governs whatever messaging and orchestration sit behind it.

Where each is the wrong answer

Every service here has a failure mode when you reach for it by reflex. So, to keep this honest:

  • Service Bus is wrong for high-volume telemetry: If you’re routing millions of fire-and-forget events and none of them individually matter, Service Bus is expensive overkill. Use Event Grid.
  • Event Grid is wrong for anything needing order: The moment sequence or guaranteed delivery matters, Event Grid stops fitting. Move to Service Bus before the gap bites.
  • Logic Apps is wrong past its complexity ceiling: A workflow with thirty actions and nested branches is a maintenance liability in the designer. Decompose it, or move it to code.
  • Code is wrong for something a citizen developer should own: Not every integration belongs in a repo. If a low-code team can own and maintain a simple connector-driven flow, hand-writing it in a Function just centralizes work that didn’t need to be centralized.

The shape of it

Azure gives you four integration services, but you don’t choose between four things. You answer two questions. Is this messaging or orchestration? And then what does failure cost, and who owns the work? Answer those, and Service Bus, Event Grid, Logic Apps, or a Function each falls out naturally, with APIM governing the front.

In practice, real platforms use several together: APIM, fronting Logic Apps, and Functions, Event Grid fanning out to Service Bus for reliable processing. So the craft of Azure messaging and orchestration isn’t picking a winner. It’s drawing clean boundaries between them.

Want the layer above this one? The Azure PaaS map puts integration in context against compute, data, and governance, and walks the five-question framework for choosing across all of them.