Azure Event Grid
Key Points
- Event Grid is Azure's system-event broker — pub/sub for discrete events (something happened) from Azure resources, your apps, or SaaS partners.
- Distinct from siblings: Service Bus = transactional FIFO work queues; Event Hubs = high-throughput streaming / telemetry; Event Grid = discrete reactive events with fan-out filtering. See Service Bus & Event Hubs.
- Topic types: System (built-in for Azure resources), Custom (your events), Domain (multi-topic hierarchical), Partner (SAP, Salesforce, etc.).
- Subscriptions route events to webhooks, Functions, Service Bus, Event Hubs, Storage Queues — with filters (subject prefix/suffix + advanced filters on payload).
- CloudEvents v1.0 schema is the cross-cloud standard; Event Grid speaks both CloudEvents and its native schema.
- Reliability: at-least-once delivery, exponential retry, dead-letter to Storage. Premium SKU adds namespaces, MQTT broker, bigger limits.
Concepts (deep dive)
What Event Grid is
A managed pub/sub system optimized for discrete event reactions — "this resource changed, react now." Not a queue; not a log. Push-based: Event Grid pushes events to subscribers.
The decision tree for the three Azure event/messaging services is:
- Event Grid — "Tell me when something happens". Reactive notifications, small JSON payloads, sub-second latency, fan-out by filter. Built-in for Azure resource events (blob created, image pushed, secret rotated).
- Service Bus — "Process this work item". Commands, business workflows, ordering, transactions, DLQ. The thing has to be done; the broker tracks that.
- Event Hubs — "Append this to my stream". Telemetry, IoT, click streams, event sourcing. Append-only log, billions of events, replay by offset.
It's normal to use all three in one system: Event Grid notifies a Function when a blob arrives, the Function drops a job onto Service Bus, the worker processes the job and emits telemetry to Event Hubs.
[ Publisher ] [ Event Grid Topic ] [ Subscribers ]
│
Storage account ──┐ ├── Subscription 1 ─► Function (filter: blob created)
Resource Group ─event─┤ │
Custom App publish ──┤ ├── Subscription 2 ─► Webhook (filter: subject /orders/*)
SaaS partner ─────┘ │
└── Subscription 3 ─► Service Bus queue (filter: type=cancelled)
Topic types
| Type | What | Example |
|---|---|---|
| System | Built-in topics for Azure resources | Storage BlobCreated, ACR ImagePushed, Resources ResourceWriteSuccess |
| Custom | Your topic for your app's events | OrderPlaced, UserSignedUp |
| Domain | Multi-tenant container of many topics, one endpoint | One domain per app, topic-per-tenant |
| Partner | Third-party SaaS publishes events to Azure | SAP S/4HANA events, Salesforce events |
// Custom topic
resource topic 'Microsoft.EventGrid/topics@2024-06-01-preview' = {
name: 'orders-topic'
location: location
properties: { inputSchema: 'CloudEventSchemaV1_0' }
}
Subscriptions
A subscription = (topic, filter, destination, retry policy, DLQ).
resource sub 'Microsoft.EventGrid/topics/eventSubscriptions@2024-06-01-preview' = {
parent: topic
name: 'order-cancelled-to-fn'
properties: {
destination: {
endpointType: 'AzureFunction'
properties: { resourceId: fn.id, maxEventsPerBatch: 10 }
}
filter: {
includedEventTypes: ['Order.Cancelled']
subjectBeginsWith: '/orders/'
advancedFilters: [{
operatorType: 'NumberGreaterThan'
key: 'data.amount'
value: 1000
}]
}
retryPolicy: {
maxDeliveryAttempts: 30
eventTimeToLiveInMinutes: 1440 // 24h
}
deadLetterDestination: {
endpointType: 'StorageBlob'
properties: { resourceId: storage.id, blobContainerName: 'eg-dlq' }
}
}
}
Filters
| Filter | What |
|---|---|
includedEventTypes | List of event-type strings (e.g. Microsoft.Storage.BlobCreated) |
subjectBeginsWith / subjectEndsWith | String prefix / suffix on the event subject |
| Advanced filters | AND/OR over event payload fields: NumberIn, StringContains, BoolEquals, etc. |
Advanced filter example:
Up to 25 advanced filters per subscription on Standard; more on Premium.
Destinations
| Endpoint | Use |
|---|---|
| Webhook (HTTPS) | Any HTTPS endpoint with the validation handshake |
| Azure Function | Direct binding (EventGridTrigger) |
| Logic App | Visual orchestration |
| Service Bus queue / topic | Buffer + transactional re-processing |
| Event Hubs | Pipe events into streaming |
| Storage Queue | Cheap durable buffer |
| Hybrid Connection | On-prem destinations |
CloudEvents v1.0
Industry-standard schema for events (CNCF). Fields: id, source, type, specversion, subject, time, data, datacontenttype.
{
"specversion": "1.0",
"type": "com.example.order.placed",
"source": "/orders/api",
"id": "evt-001",
"time": "2026-04-30T10:00:00Z",
"subject": "orders/12345",
"datacontenttype": "application/json",
"data": { "orderId": 12345, "amount": 199.99 }
}
Event Grid native schema is similar but with different field names. Default new topics to CloudEvents — portable, friendly to non-Azure consumers.
Reliability
- At-least-once delivery — subscribers may see duplicates (ensure idempotency).
- Exponential backoff retry — starts seconds, grows up to hours, gives up after
maxDeliveryAttemptsoreventTimeToLive. - Dead-letter — undeliverable events (4xx other than 408/429, exhausted retries) park in a Blob container.
- Ordering not guaranteed — events may arrive out-of-order. If you need order, include a sequence number in payload or use Service Bus sessions instead.
- Throughput — Standard SKU: ~5K events/s/topic ingress. Premium SKU (Namespaces): much higher.
Validation handshake (webhooks)
When you create a webhook subscription, Event Grid sends a subscription validation event. Your endpoint must echo back the validation code (or use URL-validation handshake) to prove ownership.
1. Create subscription → Event Grid POSTs validation event to your URL
2. Your endpoint:
- Detects validationCode in payload
- Either: returns { "validationResponse": "<code>" } in 200
- Or: GET the validationUrl provided
3. Event Grid marks subscription active
This stops random people from registering your URL as a target for their events.
Premium SKU + Namespaces
Newer "Event Grid namespaces" (Premium) bring:
- Higher limits — millions of events/s.
- Push + pull delivery — pull mode lets subscribers fetch with HTTP long-poll (no public webhook required).
- MQTT broker — IoT-style pub/sub at the edge with QoS 0/1.
- Topic spaces for hierarchical access control.
Use namespaces for IoT, high-throughput systems, or when you want pull-style consumption.
MQTT broker mode
Replaces self-managed MQTT brokers (e.g. EMQX, HiveMQ). Authenticate via X.509 certs, JWT, or AAD. Bridges MQTT messages into the rest of Azure.
Event Grid vs Service Bus vs Event Hubs
| Event Grid | Service Bus | Event Hubs | |
|---|---|---|---|
| Pattern | Discrete events; reactive | Work queue / messaging | Telemetry log / streaming |
| Throughput | High (M/s on Premium) | ~100K msg/s | Millions/s |
| Delivery | Push (and pull on Premium) | Pull | Pull (consumer groups) |
| Ordering | No | Sessions | Per-partition |
| Replay | No | No | Yes (retention) |
| DLQ | Built-in (Blob) | Built-in | Manual |
| Filtering | Subject + advanced | Subscription-level (SQL filter) | None at broker |
| Use | "Something happened, fan out" | "Process this work item" | "Stream of telemetry" |
Mental model:
- Event Grid: "the doorbell rang."
- Service Bus: "here's a job to do — atomically process it once."
- Event Hubs: "10K sensors sending readings every second; build a graph."
These often work together: Event Grid alerts on a Storage BlobCreated, drops a Service Bus message that a worker consumes; the worker emits processed metrics to Event Hubs.
.NET clients
Publishing
using Azure.Messaging.EventGrid;
using Azure.Identity;
var client = new EventGridPublisherClient(
new Uri("https://orders-topic.eastus-1.eventgrid.azure.net/api/events"),
new DefaultAzureCredential());
// CloudEvents
var evt = new CloudEvent("/orders/api", "com.acme.order.placed", new
{
orderId = 12345,
amount = 199.99
})
{
Subject = "orders/12345"
};
await client.SendEventAsync(evt);
Webhook handler in ASP.NET Core (with validation)
app.MapPost("/eg/orders", async (HttpRequest req) =>
{
using var sr = new StreamReader(req.Body);
var body = await sr.ReadToEndAsync();
// CloudEvents schema → array of events
var events = EventGridEvent.ParseMany(BinaryData.FromString(body));
foreach (var e in events)
{
// 1) Subscription validation (Event Grid native schema only)
if (e.TryGetSystemEventData(out object data) &&
data is SubscriptionValidationEventData v)
{
return Results.Ok(new { validationResponse = v.ValidationCode });
}
// 2) Real event — process idempotently
if (e.EventType == "com.acme.order.placed")
{
var payload = e.Data.ToObjectFromJson<OrderPlaced>();
await _orders.ProcessAsync(payload, e.Id /* idempotency key */);
}
}
return Results.Ok();
});
For CloudEvents schema, validation uses an OPTIONS pre-flight on the URL with WebHook-Request-Origin — respond with WebHook-Allowed-Origin header. The SDK helpers handle both schemas.
Functions trigger
[Function(nameof(OrderPlacedFn))]
public async Task Run([EventGridTrigger] CloudEvent e)
{
var order = e.Data.ToObjectFromJson<OrderPlaced>();
await _orders.ProcessAsync(order, e.Id);
}
Functions handles validation + retries + DLQ for you.
Authentication
| Path | Auth |
|---|---|
| Publisher → topic | AAD (managed identity, preferred) or topic access key |
| Topic → webhook | Webhook URL with optional client secret in query string, or AAD |
| Topic → Function/Service Bus/etc. | Managed identity assigned to topic with role on destination |
# Grant topic identity rights to push to a Function
az role assignment create \
--assignee <topic-mi-principal-id> \
--role 'EventGrid Data Sender' \
--scope <function-resource-id>
Idempotency
At-least-once = duplicates can happen. Always include a stable id (or correlation key) in the event and dedupe on the consumer side.
// Pseudo
if (await _seenIds.AddAsync(eventId)) // returns false if already seen
{
await ProcessAsync(payload);
}
Common stores for the dedup set: Redis with TTL, Cosmos DB, SQL with unique index.
Schema registry (lightweight)
For custom events, document the schema (JSON Schema or AsyncAPI). Optionally use Azure Schema Registry (lives in Event Hubs Premium) for centralized Avro/JSON schemas, but most teams just version type strings (com.acme.order.placed.v1 → v2) and document per-topic in the repo.
Cost shape
- Per million operations (ingress + delivery attempt).
- DLQ storage in your Blob account.
- Premium namespaces: per-throughput-unit baseline + ops above the unit.
A typical app generating < 1M events/month costs cents.
Code: correct vs wrong
❌ Wrong: no idempotency in handler
app.MapPost("/eg/orders", async (CloudEvent e) =>
{
await _orders.ChargeCard(e.Data.ToObjectFromJson<Order>()); // ☠ duplicate charges
});
✅ Correct: idempotent processing keyed by event id
if (await _processed.TryClaim(e.Id, ttl: TimeSpan.FromDays(7)))
{
await _orders.ChargeCard(e.Data.ToObjectFromJson<Order>());
}
❌ Wrong: ignoring subscription validation
Webhook returns 200 to validation event but doesn't echo the code. Subscription stuck in AwaitingManualAction.
✅ Correct: handle the handshake
Use SDK helpers (SubscriptionValidationEventData) or echo validationCode for native schema; respond to OPTIONS with WebHook-Allowed-Origin for CloudEvents.
❌ Wrong: relying on order
Event Grid doesn't guarantee order.
✅ Correct: use sequence numbers or Service Bus sessions
Include sequenceNumber in payload; reject out-of-order updates. Or use Service Bus sessions when ordering is required.
Design patterns for this topic
Pattern 1 — "System events as glue"
- Intent: react to Azure resource changes (BlobCreated → Function processes file).
Pattern 2 — "Custom topic as fan-out hub"
- Intent: many subscribers to one event with filtering.
Pattern 3 — "Event Grid → Service Bus for durable work"
- Intent: combine fan-out + durable processing.
Pattern 4 — "DLQ + alert + replay"
- Intent: never lose events; observe and recover.
Pattern 5 — "CloudEvents schema by default"
- Intent: portable, cross-cloud-friendly.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| Event Grid | Push delivery; cheap; built-in retry + DLQ | No replay; no ordering |
| System topics | Free Azure-resource events | Schema dictated by source |
| Custom topics | Your domain events | You own schema versioning |
| MQTT (Premium) | IoT broker built-in | Premium cost |
| CloudEvents | Industry standard | Slightly more verbose |
When to use / when to avoid
- Use for reactive integration to Azure resource changes.
- Use for fan-out to multiple subscribers with per-subscriber filters.
- Use as front-end before Service Bus when you want filtering + durability.
- Avoid for high-throughput telemetry streaming — Event Hubs.
- Avoid for FIFO ordered work — Service Bus sessions.
- Avoid when you need replay — Event Hubs (or persist your own log).
Interview Q&A
Q1. Event Grid vs Service Bus vs Event Hubs? Event Grid: discrete events, push, fan-out. Service Bus: transactional work queue, FIFO via sessions. Event Hubs: high-throughput streaming with replay.
Q2. Topic types? System (Azure resources), Custom (your events), Domain (multi-topic), Partner (SaaS).
Q3. Subscription filters? Event types, subject begins/ends with, advanced filters (StringIn, NumberGreaterThan, BoolEquals) over payload.
Q4. CloudEvents vs native schema? CloudEvents v1.0 = CNCF standard with type, source, data, etc. Native = older Microsoft schema. Prefer CloudEvents.
Q5. Delivery semantics? At-least-once. Duplicates possible. Order not guaranteed.
Q6. Retry + DLQ? Exponential backoff up to maxDeliveryAttempts or TTL. Then dead-letter to Storage Blob.
Q7. Subscription validation? Event Grid sends validation event; your endpoint echoes the code (native schema) or responds to OPTIONS with allowed-origin header (CloudEvents).
Q8. Idempotency? Use event id as dedup key; store in Redis/Cosmos with TTL.
Q9. MQTT broker mode? Premium namespaces support MQTT 3.1.⅕ — IoT-class pub/sub.
Q10. Premium namespace benefits? Higher throughput, MQTT, pull-style delivery, topic spaces.
Q11. Auth from publisher? Managed identity preferred over access keys.
Q12. When pair with Service Bus? Event Grid for fan-out + filtering at edge; Service Bus for durable, transactional consumer-side processing.
Gotchas / common mistakes
- ⚠️ No idempotency → duplicate side-effects on retry.
- ⚠️ Subscription validation ignored → subscription never activates.
- ⚠️ Assuming order — events may arrive out-of-order.
- ⚠️ No DLQ configured — events vanish after retries.
- ⚠️ Tight coupling on schema — version your event
typestrings. - ⚠️ Webhook open to internet without secret — anyone can spam it.
- ⚠️ Using Event Grid for streaming telemetry — wrong tool; use Event Hubs.