Azure offers three services with similar names but very different roles: Service Bus, Event Hubs, and Event Grid.
They all move data between parts of your system, so it is easy to assume they are interchangeable. They are not.
I have already covered Azure Service Bus in detail - the reliable message broker for commands and workflows between services.
This post covers the other two and then shows you exactly when to reach for each.
In this post, we will explore:
- Azure Event Grid: how it works
- Using Azure Event Grid in .NET
- Azure Event Hubs: how it works
- Using Azure Event Hubs in .NET
- Service Bus vs Event Hubs vs Event Grid: when to use each
Let's dive in.
Azure Event Grid: How It Works
Azure Event Grid is a fully managed event routing service.
It follows a simple idea: something happens, and Event Grid delivers a notification to everyone who cares.
That "something" is an event - a small message that says a change occurred:
- A blob was uploaded
- A resource was created
- An order was placed.
Event Grid does not store your data or run your logic. It takes events from publishers and pushes them to subscribers, fast and at scale.
Here is the model:
Azure Event Grid consists of four parts:
- Publishers - where events come from. Azure services like Blob Storage and Resource Groups emit system events automatically. Your own application publishes custom events to a custom topic.
- Topic - the endpoint events are sent to. A system topic for Azure events, or a custom topic for your own.
- Event subscriptions - the rules that connect a topic to a handler. Each subscription can be filtered by event type or subject, so a handler receives only the events it requested.
- Event handlers - where events are delivered: an HTTP webhook, an Azure Function, a Service Bus queue, an Event Hub, and more.
How Event Grid Differs from Service Bus
The key difference is push versus pull.
Service Bus is a broker you pull from: your consumer connects, locks a message, processes it, and completes it. The consumer controls the pace.
Event Grid pushes: when an event occurs, Event Grid makes an HTTP call to your handler. You do not poll - you expose an endpoint and wait to be called.
A few more differences matter:
- Reactive notifications, not message transport. An Event Grid event is a lightweight signal ("blob X was created"), not a payload meant to carry megabytes of data. Service Bus messages can carry the full payload your consumer needs.
- No ordering, no sessions. Event Grid does not guarantee order. Service Bus offers FIFO ordering through sessions.
- Retries and dead-lettering work differently. Event Grid retries a failed HTTP delivery with backoff for up to 24 hours, then dead-letters to a Storage account you configure. Service Bus has a built-in dead-letter sub-queue on every queue and subscription.
- Serverless and pay-per-event. Event Grid has no namespace to provision or scale - you pay per operation. Service Bus runs in a provisioned namespace.
Use Event Grid when you want many parts of your system to react to something that happened, especially Azure resource changes.
Using Azure Event Grid in .NET
Working with Event Grid in .NET has two sides: publishing events and handling them.
First, install the following client NuGet package:
bashdotnet add package Azure.Messaging.EventGrid
Publishing Events
To publish custom events, create an EventGridPublisherClient with your topic endpoint and key, then send one or multiple EventGridEvent objects:
csharpvar client = new EventGridPublisherClient( new Uri("https://my-topic.westeurope-1.eventgrid.azure.net/api/events"), new AzureKeyCredential(topicKey)); var orderPlaced = new EventGridEvent( subject: $"orders/{order.Id}", eventType: "Shop.OrderPlaced", dataVersion: "1.0", data: new OrderPlacedEvent(order.Id, order.Total, order.CustomerId)); await client.SendEventAsync(orderPlaced);
Each event carries:
subject- a path that describes what the event is about. Subscribers can filter on it.eventType- the category of event, used for routing and filtering.dataVersion- the schema version of your payload, so handlers can evolve safely.data- your payload, serialized to JSON.
In production, never hard-code the topic key. Pull it from configuration, or use DefaultAzureCredential with a managed identity.
Note: Event Grid can also send and receive events in the standard CloudEvents 1.0 schema instead of its native
EventGridEventschema. Use CloudEvents when your events cross systems that expect that format.
Handling Events in a Webhook
Because Event Grid pushes over HTTP, a handler is just an endpoint that accepts a POST.
Before it delivers any events, Event Grid sends a one-time validation event to prove you own the endpoint. You must echo back the validation code.
Here is a minimal API endpoint that handles both the handshake and real events:
csharpapp.MapPost("/events", async (HttpRequest request) => { var events = EventGridEvent.ParseMany( await BinaryData.FromStreamAsync(request.Body)); foreach (var gridEvent in events) { if (gridEvent.TryGetSystemEventData(out var systemEvent)) { switch (systemEvent) { case SubscriptionValidationEventData validation: // Echo the code back to complete the handshake return Results.Ok(new SubscriptionValidationResponse { ValidationResponse = validation.ValidationCode }); case StorageBlobCreatedEventData blobCreated: await ProcessNewBlobAsync(blobCreated.Url); break; } } } return Results.Ok(); });
What happens here:
EventGridEvent.ParseManydeserializes the request body into one or more events - Event Grid can batch them.TryGetSystemEventDatarecognizes built-in Azure events and gives you a strongly typed object.- On a
SubscriptionValidationEventData, we return theValidationCode. Event Grid sees it and activates the subscription. - On a
StorageBlobCreatedEventData, we react - here, processing a blob that was just uploaded.
This is the canonical Event Grid scenario: a file lands in Blob Storage, Storage emits a BlobCreated event, and your service reacts with no polling at all.
Note: there is no first-party local emulator for Event Grid push delivery. To develop locally, expose your endpoint with a tunneling tool and point an event subscription at it, or run the Azure "Event Grid Viewer" sample to watch events arrive.
If you host on Azure Functions, the EventGridTrigger binding handles the validation handshake for you, so your function receives only real events:
csharp[Function("OnBlobCreated")] public void Run([EventGridTrigger] EventGridEvent gridEvent) { // The validation handshake is handled by the binding }
A newer option, Event Grid Namespaces, flips the model to pull delivery and adds MQTT support, which suits IoT and high-volume scenarios.
It uses a different client, Azure.Messaging.EventGrid.Namespaces.
The classic push model above is the right default for reacting to Azure and domain events.
Azure Event Hubs: How It Works
Azure Event Hubs is a big-data streaming platform built to ingest millions of events per second.
Where Service Bus moves discrete messages and Event Grid routes notifications, Event Hubs is a pipe for high-volume event streams - telemetry, logs, clickstreams, IoT signals, and metrics.
The core idea is an append-only log. Producers append events to the end, and consumers read forward from a position they track themselves.
Here is the model:
The building blocks are:
- Partitions - the event hub is split into partitions, each an ordered log. More partitions mean more parallelism. Events with the same partition key always land in the same partition, which preserves their order.
- Consumer groups - a named, independent view over the whole stream. A real-time dashboard and a batch analytics job each get their own consumer group and read the same data at their own pace.
- Offsets and checkpoints - each event has an offset, its position in the partition. A consumer periodically saves a checkpoint so it can resume where it left off after a restart.
- Retention - events stay in the hub for a configured window, from one day to several. Within that window, any consumer can replay them.
How Event Hubs Differ from Service Bus
Service Bus is a queue: messages are delivered, processed, and deleted. Event Hubs is a log: events are retained and can be read repeatedly by different consumers.
The differences follow from that:
- Replay. Event Hubs lets you rewind and reprocess the stream within the retention window. A Service Bus message is gone once it is completed.
- The consumer tracks position. With Event Hubs, the client owns its checkpoint. With Service Bus, the broker tracks each message with locks and delivery counts.
- Ordering by partition. Event Hubs guarantees order within a partition. Service Bus guarantees order through sessions.
- No per-message acknowledgement, no dead-letter queue. Event Hubs does not lock or dead-letter individual events. If your handler fails, you can either skip ahead or reprocess from a checkpoint.
- Throughput. Event Hubs targets millions of events per second; Service Bus targets thousands of messages per second with richer per-message features.
Event Hubs also speaks the Apache Kafka protocol, so existing Kafka producers and consumers can connect to it with a connection-string change. If you have run Kafka pipelines, the model will feel familiar.
Using Azure Event Hubs in .NET
Like Service Bus, Event Hubs works cleanly with .NET Aspire, and it ships with a local emulator, so you can develop without an Azure subscription.
To set up Aspire from scratch, see Getting Started with .NET Aspire. The Aspire and
azddeployment flow is the same one used in the Azure Service Bus article.
In the Aspire AppHost, add the Event Hubs resource and run it as an emulator:
bashdotnet add package Aspire.Hosting.Azure.EventHubs
csharpvar builder = DistributedApplication.CreateBuilder(args); var eventHubs = builder.AddAzureEventHubs("event-hubs") .RunAsEmulator(); eventHubs.AddHub("telemetry"); builder.AddProject<Projects.Telemetry_Producer>("producer") .WithReference(eventHubs); builder.AddProject<Projects.Telemetry_Processor>("processor") .WithReference(eventHubs); builder.Build().Run();
RunAsEmulator() starts the Event Hubs emulator in Docker, and AddHub creates a hub named telemetry. Aspire injects the connection string into both services.
Publishing Events
In the producer service, register the client, then publish events in batches for throughput:
bashdotnet add package Aspire.Azure.Messaging.EventHubs
csharpbuilder.AddAzureEventHubProducerClient("event-hubs", settings => settings.EventHubName = "telemetry");
csharppublic class TelemetryPublisher(EventHubProducerClient client) { public async Task PublishAsync(IReadOnlyList<DeviceReading> readings) { using EventDataBatch batch = await client.CreateBatchAsync(); foreach (var reading in readings) { var json = JsonSerializer.Serialize(reading); if (!batch.TryAdd(new EventData(json))) { // Batch is full - send it and start a new one await client.SendAsync(batch); batch = await client.CreateBatchAsync(); batch.TryAdd(new EventData(json)); } } await client.SendAsync(batch); } }
Batching is what makes Event Hubs fast.
Instead of one network call per event, you pack many events into a single EventDataBatch and send them together.
TryAdd returns false when the batch reaches its size limit, so we flush it and start a fresh one.
Consuming Events
To read the stream, use EventProcessorClient.
It balances partitions across instances and stores checkpoints in Blob Storage, so several copies of your service share the work:
csharppublic class TelemetryProcessor( EventProcessorClient processor, ILogger<TelemetryProcessor> logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { processor.ProcessEventAsync += async args => { var json = args.Data.EventBody.ToString(); var reading = JsonSerializer.Deserialize<DeviceReading>(json); // ... process the reading await args.UpdateCheckpointAsync(args.CancellationToken); }; processor.ProcessErrorAsync += args => { logger.LogError(args.Exception, "Error on partition {PartitionId}", args.PartitionId); return Task.CompletedTask; }; await processor.StartProcessingAsync(stoppingToken); while (!stoppingToken.IsCancellationRequested) { } await processor.StopProcessingAsync(CancellationToken.None); } }
Two handlers drive the processor:
ProcessEventAsyncfires for each event. We deserialize it, process it, then callUpdateCheckpointAsyncto record our position. After a restart, the processor resumes from the last checkpoint instead of replaying everything.ProcessErrorAsyncfires on partition-level errors, like a lost lease.
Checkpointing on every single event is expensive.
In a high-throughput pipeline, checkpoint every few seconds or every N events, and accept that a restart may reprocess a small batch - which is exactly why your processing should be idempotent.
Note:
EventProcessorClientneeds a Blob Storage container to hold its checkpoints and partition leases. Aspire wires one up alongside the emulator locally; in production, it is a real Storage account.
Service Bus vs Event Hubs vs Event Grid: When to Use Each
The three services overlap just enough to be confusing. Here is how they line up side by side:
| Azure Service Bus | Azure Event Hubs | Azure Event Grid | |
|---|---|---|---|
| Primary purpose | Reliable message broker | High-throughput streaming | Reactive event routing |
| Communication | Pull (competing consumers) | Pull (partitioned log) | Push (HTTP to handlers) |
| Pattern | Queues, Topics/Subscriptions | Partitions + consumer groups | Topics + event subscriptions |
| Throughput | Thousands/sec | Millions/sec | High, per-event |
| Ordering | FIFO with sessions | Per partition | None |
| Delivery / retries | At-least-once, locks, DLQ | At-least-once, checkpoints, no DLQ | At-least-once, retry + dead-letter to Storage |
| Replay | No (consume-and-delete) | Yes (retention window) | No |
| Max event size | 256 KB (Premium: 100 MB) | 1 MB | 1 MB |
| Protocols | AMQP, HTTP | AMQP, Kafka, HTTP | HTTP, MQTT (Namespaces) |
| Pricing model | Provisioned namespace | Throughput / processing units | Pay per operation |
| Best for | Commands and workflows | Telemetry, logs, analytics | Reacting to Azure and domain events |
The quickest way to choose is to ask what you actually need:
Azure Service Bus
Use it when you need reliable, ordered, transactional messaging between services - commands, workflows, and business events where every message matters and must not be lost.
Avoid it when you are ingesting massive event streams (Event Hubs is built for that), or simply reacting to Azure resource changes (Event Grid is simpler and cheaper).
Azure Event Hubs
Use it when you ingest high-volume telemetry, logs, metrics, or clickstream data and want multiple independent consumers plus the ability to replay.
Avoid it when you need per-message retries, dead-lettering, or guaranteed FIFO across the whole stream - those are Service Bus features.
Azure Event Grid
Use it when you want to react to events with minimal infrastructure - a blob is uploaded, a resource is created, or your app raises a domain event that several handlers should receive.
Avoid it when you need to carry large payloads, guarantee order, or pull at your own pace - reach for Service Bus or Event Hubs instead.
A few concrete scenarios make it click:
- A checkout that must update stock, charge a payment, and notify the customer - Service Bus. Each step is a command that must not be lost, and order matters.
- Tracking every page view and click across your site for a live dashboard - Event Hubs. Millions of events, multiple consumers, replayable.
- Generating a thumbnail whenever a product image is uploaded - Event Grid. React to the Storage
BlobCreatedevent, with no polling and nothing to run.
Many real systems use all three together: Event Grid reacts to infrastructure events, Event Hubs ingests the telemetry firehose, and Service Bus runs reliable business workflows.
Summary
Azure's three messaging services solve different problems.
Let's recap the key takeaways:
- Azure Service Bus is the reliable broker. Use it for commands and workflows between services, where ordering, retries, and dead-lettering matter. You pull messages and complete them one by one.
- Azure Event Hubs is the streaming pipe. Use it to ingest millions of events per second, with partitions, consumer groups, and replay. The consumer tracks its own position with checkpoints.
- Azure Event Grid is the reactive router. Use it to push lightweight notifications to handlers when something happens, especially Azure resource changes. It is serverless and pay-per-event.
- The one-line rule: react to events with Event Grid, stream events with Event Hubs, and process reliable messages with Service Bus.
There is no single best choice - the right service depends on whether you need reliable delivery, raw throughput, or simple reactivity. Many systems use all three, each for what it does best.
I am building a deep-dive System Design course.
It will be better than anything you have seen so far:
- Reading
- Watching videos
- Designing real systems in the browser
- Taking tests
Join the waitlist here to get early access and a launch discount.
Hope you find this newsletter useful. See you next time.


Comments