Event Grid — Advanced Event Filtering and Routing Patterns
Why Event Filtering Matters
Without filtering, every subscriber receives every event. This creates three problems:
- Cost — Event Grid charges per event delivery. If a subscriber only cares about 5% of events, you're paying 20x more than necessary.
- Performance — Subscribers waste compute cycles receiving, deserializing, and discarding irrelevant events. At scale, this adds up to real latency and resource pressure.
- Separation of concerns — When filtering happens inside subscriber code, every handler becomes coupled to the full event schema. Moving filters to the subscription level keeps handlers focused on business logic.
Event Grid's server-side filtering evaluates events before delivery. Events that don't match a subscription's filter are never sent — no delivery attempt, no charge, no wasted bandwidth.
Architecture: Filtering Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ Event Grid Filtering Architecture │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐
│ Event Source │ (e.g., Storage Account, Custom App, IoT Hub)
│ │
│ Publishes ALL │
│ events to topic │
└────────┬─────────┘
│
│ CloudEvents / Event Grid Schema
▼
┌──────────────────────────────────────────────────────────────────┐
│ Event Grid Topic │
│ │
│ Incoming Event: │
│ { │
│ "type": "OrderCreated", │
│ "subject": "orders/us-west/12345", │
│ "data": { "amount": 5000, "region": "US", "priority": 1 } │
│ } │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Filter Evaluation Engine │ │
│ │ │ │
│ │ Sub 1 filter: subject beginsWith "orders/us-west" ✓ │ │
│ │ Sub 2 filter: data.amount > 1000 ✓ │ │
│ │ Sub 3 filter: type = "OrderCancelled" ✗ │ │
│ │ Sub 4 filter: data.region in ["EU","APAC"] ✗ │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────┬───────────────┬───────────────┬───────────────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Sub 1: ✓ │ │ Sub 2: ✓ │ │ Sub 3: ✗ │ │ Sub 4: ✗ │
│ US-West │ │ High-Value │ │ Cancellation│ │ EU/APAC │
│ Handler │ │ Handler │ │ Handler │ │ Handler │
│ │ │ │ │ (no event) │ │ (no event) │
│ DELIVERED │ │ DELIVERED │ │ SKIPPED │ │ SKIPPED │
└─────────────┘ └─────────────┘ └──────────────┘ └─────────────┘
Types of Filters
Event Grid supports three filter categories, which can be combined on a single subscription.
1. Event Type Filtering
The simplest filter — match on the eventType (or type in CloudEvents) field.
{
"includedEventTypes": [
"Microsoft.Storage.BlobCreated",
"Microsoft.Storage.BlobDeleted"
]
}
Use this when a subscriber only handles specific event types from a source that emits many.
2. Subject Filtering
Match on the subject field using prefix and/or suffix:
| Operator | Use Case | Example |
|---|---|---|
subjectBeginsWith | Route by hierarchy/path | "/orders/us-west" matches subjects starting with that path |
subjectEndsWith | Route by file type or category | ".jpg" matches subjects ending in .jpg |
{
"subjectBeginsWith": "/orders/us-west",
"subjectEndsWith": "/high-priority"
}
Both conditions must be true (AND logic). Subject filtering is case-sensitive by default — set isSubjectCaseSensitive: false to change this.
3. Advanced Filters
Advanced filters operate on any field in the event payload, including nested data.* properties. You can have up to 25 advanced filters per subscription, each with up to 25 values.
| Operator | Data Type | Description |
|---|---|---|
NumberGreaterThan | Numeric | Value > threshold |
NumberGreaterThanOrEquals | Numeric | Value >= threshold |
NumberLessThan | Numeric | Value < threshold |
NumberLessThanOrEquals | Numeric | Value <= threshold |
NumberIn | Numeric | Value is in the specified set |
NumberNotIn | Numeric | Value is NOT in the specified set |
NumberInRange | Numeric | Value falls within a range |
NumberNotInRange | Numeric | Value falls outside a range |
StringContains | String | Value contains any of the specified strings |
StringNotContains | String | Value does NOT contain any of the specified strings |
StringBeginsWith | String | Value starts with any of the specified strings |
StringNotBeginsWith | String | Value does NOT start with any of the specified strings |
StringEndsWith | String | Value ends with any of the specified strings |
StringNotEndsWith | String | Value does NOT end with any of the specified strings |
StringIn | String | Exact match against a set of values |
StringNotIn | String | Value is NOT in the specified set |
BoolEquals | Boolean | Value equals true or false |
IsNullOrUndefined | Any | Field is null or missing |
IsNotNull | Any | Field exists and is not null |
Key rule: Multiple advanced filters use AND logic — all must match for the event to be delivered.
Step-by-Step Implementation
Prerequisites
# Ensure Event Grid extension is registered
az provider register --namespace Microsoft.EventGrid
# Create resource group and topic
az group create --name rg-eventgrid-demo --location eastus2
az eventgrid topic create \
--name orders-topic \
--resource-group rg-eventgrid-demo \
--location eastus2 \
--input-schema cloudeventschemav1_0
Creating Subscriptions with Subject Filters
# Route US-West orders to a specific webhook
az eventgrid event-subscription create \
--name us-west-orders \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--endpoint "https://us-west-handler.azurewebsites.net/api/orders" \
--subject-begins-with "/orders/us-west" \
--subject-ends-with "/new"
Creating Subscriptions with Event Type Filters
# Only receive OrderCreated and OrderUpdated events
az eventgrid event-subscription create \
--name order-lifecycle \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--endpoint "https://lifecycle-handler.azurewebsites.net/api/events" \
--included-event-types "OrderCreated" "OrderUpdated"
Creating Subscriptions with Advanced Filters
# High-value orders (amount > 1000) from US or EU regions
az eventgrid event-subscription create \
--name high-value-orders \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--endpoint "https://high-value-handler.azurewebsites.net/api/orders" \
--included-event-types "OrderCreated" \
--advanced-filter data.amount NumberGreaterThan 1000 \
--advanced-filter data.region StringIn US EU
C# Implementation: Creating Filtered Subscriptions Programmatically
using Azure;
using Azure.Messaging.EventGrid.Namespaces;
using Azure.ResourceManager;
using Azure.ResourceManager.EventGrid;
using Azure.ResourceManager.EventGrid.Models;
public class EventGridFilterService
{
private readonly ArmClient _armClient;
public EventGridFilterService(ArmClient armClient)
{
_armClient = armClient;
}
public async Task CreateHighValueOrderSubscriptionAsync(string topicResourceId)
{
var topicResource = _armClient.GetEventGridTopicResource(
new ResourceIdentifier(topicResourceId));
var subscriptionData = new EventGridSubscriptionData
{
Destination = new WebHookEventSubscriptionDestination
{
Endpoint = new Uri("https://high-value-handler.azurewebsites.net/api/orders")
},
Filter = new EventSubscriptionFilter
{
SubjectBeginsWith = "/orders/",
IncludedEventTypes = { "OrderCreated", "OrderUpdated" },
AdvancedFilters =
{
new NumberGreaterThanAdvancedFilter("data.amount") { Value = 1000 },
new StringInAdvancedFilter("data.region") { Values = { "US", "EU" } },
new BoolEqualsAdvancedFilter("data.isVerified") { Value = true }
}
},
RetryPolicy = new EventSubscriptionRetryPolicy
{
MaxDeliveryAttempts = 30,
EventTimeToLiveInMinutes = 1440
}
};
await topicResource.GetEventGridTopicEventSubscriptions()
.CreateOrUpdateAsync(WaitUntil.Completed, "high-value-orders", subscriptionData);
}
}
C# Event Handler with Filter Validation
using Azure.Messaging;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/orders")]
public class OrderEventsController : ControllerBase
{
private readonly ILogger<OrderEventsController> _logger;
public OrderEventsController(ILogger<OrderEventsController> logger)
{
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> HandleOrderEvent([FromBody] CloudEvent[] events)
{
foreach (var cloudEvent in events)
{
_logger.LogInformation(
"Received {Type} for subject {Subject}",
cloudEvent.Type, cloudEvent.Subject);
var orderData = cloudEvent.Data.ToObjectFromJson<OrderEventData>();
// Event Grid already filtered — we know amount > 1000 and region is US/EU
await ProcessHighValueOrder(orderData);
}
return Ok();
}
private Task ProcessHighValueOrder(OrderEventData order)
{
_logger.LogInformation(
"Processing high-value order {OrderId}: ${Amount} in {Region}",
order.OrderId, order.Amount, order.Region);
// Business logic here
return Task.CompletedTask;
}
}
public record OrderEventData(string OrderId, decimal Amount, string Region, bool IsVerified);
Complex Filter Combinations
AND Logic (Default Behavior)
All filters on a subscription are combined with AND. Every condition must be true:
# Event must match ALL of these:
# - Subject starts with /orders/
# - Event type is OrderCreated
# - data.amount > 500
# - data.region is "US" or "EU"
# - data.isVerified is true
az eventgrid event-subscription create \
--name strict-filter-sub \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--endpoint "https://handler.azurewebsites.net/api/events" \
--subject-begins-with "/orders/" \
--included-event-types "OrderCreated" \
--advanced-filter data.amount NumberGreaterThan 500 \
--advanced-filter data.region StringIn US EU \
--advanced-filter data.isVerified BoolEquals true
Achieving OR Logic
Event Grid doesn't natively support OR between filters. Use one of these patterns:
Pattern 1: Multiple subscriptions pointing to the same endpoint
# OR: amount > 5000 OR priority = "critical"
# Subscription A: high amount
az eventgrid event-subscription create \
--name high-amount-orders \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--endpoint "https://priority-handler.azurewebsites.net/api/events" \
--advanced-filter data.amount NumberGreaterThan 5000
# Subscription B: critical priority
az eventgrid event-subscription create \
--name critical-priority-orders \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--endpoint "https://priority-handler.azurewebsites.net/api/events" \
--advanced-filter data.priority StringIn critical
Note: The handler may receive duplicates if an event matches both subscriptions. Implement idempotency using the event ID.
Pattern 2: StringIn / NumberIn for OR within a single field
# data.region is "US" OR "EU" OR "APAC" — this is native OR within one filter
--advanced-filter data.region StringIn US EU APAC
Content-Based Routing Patterns
Route events to different endpoints based on event content without subscriber-side logic:
# Route by order value tier
# Tier 1: Premium orders (> $10,000) → dedicated handler
az eventgrid event-subscription create \
--name premium-orders \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--endpoint "https://premium-handler.azurewebsites.net/api/orders" \
--advanced-filter data.amount NumberGreaterThan 10000
# Tier 2: Standard orders ($100–$10,000) → standard handler
az eventgrid event-subscription create \
--name standard-orders \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--endpoint "https://standard-handler.azurewebsites.net/api/orders" \
--advanced-filter data.amount NumberGreaterThan 100 \
--advanced-filter data.amount NumberLessThanOrEquals 10000
# Tier 3: Micro orders (≤ $100) → batch processor
az eventgrid event-subscription create \
--name micro-orders \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--endpoint "https://batch-handler.azurewebsites.net/api/orders" \
--advanced-filter data.amount NumberLessThanOrEquals 100
Routing to Different Destination Types
// Create subscriptions routing to different Azure services
public async Task SetupContentBasedRoutingAsync(string topicResourceId)
{
var topic = _armClient.GetEventGridTopicResource(new ResourceIdentifier(topicResourceId));
var subscriptions = topic.GetEventGridTopicEventSubscriptions();
// High-priority → Service Bus queue for guaranteed processing
await subscriptions.CreateOrUpdateAsync(WaitUntil.Completed, "high-priority-to-servicebus",
new EventGridSubscriptionData
{
Destination = new ServiceBusQueueEventSubscriptionDestination
{
ResourceId = new ResourceIdentifier("/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.ServiceBus/namespaces/my-ns/queues/high-priority")
},
Filter = new EventSubscriptionFilter
{
AdvancedFilters = { new StringInAdvancedFilter("data.priority") { Values = { "critical", "high" } } }
}
});
// Analytics events → Event Hub for stream processing
await subscriptions.CreateOrUpdateAsync(WaitUntil.Completed, "analytics-to-eventhub",
new EventGridSubscriptionData
{
Destination = new EventHubEventSubscriptionDestination
{
ResourceId = new ResourceIdentifier("/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventHub/namespaces/my-ns/eventhubs/analytics")
},
Filter = new EventSubscriptionFilter
{
IncludedEventTypes = { "PageViewed", "ButtonClicked", "SessionStarted" }
}
});
}
Dead Letter Handling
Events that fail delivery after all retry attempts — or events that can't be delivered due to configuration errors — go to a dead letter destination.
# Create a storage container for dead letters
az storage container create \
--name deadletters \
--account-name mystorageaccount
# Create subscription with dead lettering enabled
az eventgrid event-subscription create \
--name orders-with-deadletter \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--endpoint "https://handler.azurewebsites.net/api/orders" \
--advanced-filter data.amount NumberGreaterThan 1000 \
--deadletter-endpoint "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.Storage/storageAccounts/mystorageaccount/blobServices/default/containers/deadletters" \
--max-delivery-attempts 10 \
--event-ttl 1440
Processing Dead Letters
using Azure.Storage.Blobs;
public class DeadLetterProcessor
{
private readonly BlobContainerClient _containerClient;
private readonly ILogger<DeadLetterProcessor> _logger;
public DeadLetterProcessor(BlobServiceClient blobClient, ILogger<DeadLetterProcessor> logger)
{
_containerClient = blobClient.GetBlobContainerClient("deadletters");
_logger = logger;
}
public async Task ProcessDeadLettersAsync()
{
await foreach (var blob in _containerClient.GetBlobsAsync())
{
var blobClient = _containerClient.GetBlobClient(blob.Name);
var content = await blobClient.DownloadContentAsync();
var deadLetter = content.Value.Content.ToObjectFromJson<DeadLetterEvent>();
_logger.LogWarning(
"Dead letter: {EventType} failed with {Error}. Last attempt: {LastAttempt}",
deadLetter.EventType,
deadLetter.DeadLetterReason,
deadLetter.LastDeliveryAttemptTime);
// Retry, alert, or archive based on reason
if (deadLetter.DeadLetterReason == "MaxDeliveryAttemptsExceeded")
await RetryWithBackoff(deadLetter);
}
}
private Task RetryWithBackoff(DeadLetterEvent evt) => Task.CompletedTask;
}
public record DeadLetterEvent(string EventType, string DeadLetterReason, DateTime LastDeliveryAttemptTime);
Real-World Scenarios
Scenario 1: E-Commerce Order Routing by Value and Region
# Premium US orders → white-glove fulfillment team
az eventgrid event-subscription create \
--name premium-us-fulfillment \
--source-resource-id $TOPIC_ID \
--endpoint "https://premium-fulfillment.azurewebsites.net/api/orders" \
--subject-begins-with "/orders/us" \
--advanced-filter data.orderTotal NumberGreaterThan 5000 \
--advanced-filter data.customerTier StringIn platinum gold
# EU orders → GDPR-compliant handler in EU region
az eventgrid event-subscription create \
--name eu-gdpr-handler \
--source-resource-id $TOPIC_ID \
--endpoint "https://eu-handler.azurewebsites.net/api/orders" \
--advanced-filter data.region StringIn EU-West EU-North EU-Central \
--advanced-filter data.containsPII BoolEquals true
Scenario 2: IoT Device Filtering by Type and Severity
# Critical temperature alerts from industrial sensors
az eventgrid event-subscription create \
--name critical-temp-alerts \
--source-resource-id $TOPIC_ID \
--endpoint "https://alert-handler.azurewebsites.net/api/iot" \
--included-event-types "TemperatureReading" \
--advanced-filter data.severity StringIn critical \
--advanced-filter data.deviceType StringIn industrial-sensor \
--advanced-filter data.temperature NumberGreaterThan 85
# Battery-low events for all mobile devices
az eventgrid event-subscription create \
--name battery-monitoring \
--source-resource-id $TOPIC_ID \
--endpoint "https://device-mgmt.azurewebsites.net/api/battery" \
--included-event-types "BatteryStatus" \
--advanced-filter data.batteryPercent NumberLessThan 20 \
--advanced-filter data.deviceType StringIn mobile tablet wearable
Scenario 3: Multi-Tenant Event Routing
# Route events to tenant-specific handlers
az eventgrid event-subscription create \
--name tenant-acme-corp \
--source-resource-id $TOPIC_ID \
--endpoint "https://acme-handler.azurewebsites.net/api/events" \
--advanced-filter data.tenantId StringIn tenant-acme-001
az eventgrid event-subscription create \
--name tenant-globex \
--source-resource-id $TOPIC_ID \
--endpoint "https://globex-handler.azurewebsites.net/api/events" \
--advanced-filter data.tenantId StringIn tenant-globex-002
Testing Filters
Publish Test Events with Azure CLI
# Get topic endpoint and key
TOPIC_ENDPOINT=$(az eventgrid topic show --name orders-topic -g rg-eventgrid-demo --query "endpoint" -o tsv)
TOPIC_KEY=$(az eventgrid topic key list --name orders-topic -g rg-eventgrid-demo --query "key1" -o tsv)
# Publish a test event that should match high-value filter
curl -X POST "$TOPIC_ENDPOINT" \
-H "aeg-sas-key: $TOPIC_KEY" \
-H "Content-Type: application/cloudevents+json" \
-d '{
"specversion": "1.0",
"type": "OrderCreated",
"source": "/orders/us-west",
"id": "test-001",
"subject": "/orders/us-west/12345",
"data": {
"orderId": "12345",
"amount": 5500,
"region": "US",
"isVerified": true,
"priority": "high"
}
}'
# Publish an event that should NOT match (amount too low)
curl -X POST "$TOPIC_ENDPOINT" \
-H "aeg-sas-key: $TOPIC_KEY" \
-H "Content-Type: application/cloudevents+json" \
-d '{
"specversion": "1.0",
"type": "OrderCreated",
"source": "/orders/us-west",
"id": "test-002",
"subject": "/orders/us-west/67890",
"data": {
"orderId": "67890",
"amount": 50,
"region": "US",
"isVerified": true,
"priority": "low"
}
}'
Verify Delivery with CLI
# Check subscription delivery metrics
az eventgrid event-subscription show \
--name high-value-orders \
--source-resource-id "/subscriptions/{sub-id}/resourceGroups/rg-eventgrid-demo/providers/Microsoft.EventGrid/topics/orders-topic" \
--include-full-endpoint-url \
--query "{deliveredEvents: provisioningState, endpoint: destination.endpointUrl}"
Monitoring with KQL Queries
Track Filtered vs Delivered Events
// Events published vs delivered per subscription (last 24h)
AegDeliverySuccessCount
| where TimeGenerated > ago(24h)
| summarize DeliveredCount = sum(Count) by EventSubscriptionName
| join kind=leftouter (
AegPublishSuccessCount
| where TimeGenerated > ago(24h)
| summarize PublishedCount = sum(Count) by TopicName
) on $left.EventSubscriptionName == $right.TopicName
| project EventSubscriptionName, DeliveredCount, PublishedCount,
FilterEfficiency = round(100.0 - (DeliveredCount * 100.0 / PublishedCount), 2)
Detect Filter Misconfigurations
// Subscriptions receiving zero events (possible misconfigured filter)
AegDeliverySuccessCount
| where TimeGenerated > ago(7d)
| summarize TotalDelivered = sum(Count) by EventSubscriptionName
| where TotalDelivered == 0
| project EventSubscriptionName, TotalDelivered,
Status = "⚠️ No events delivered in 7 days — check filter configuration"
Dead Letter Monitoring
// Dead letter events by reason
AegDeadLetterCount
| where TimeGenerated > ago(24h)
| summarize DeadLetterCount = sum(Count) by EventSubscriptionName, DeadLetterReason
| order by DeadLetterCount desc
| project EventSubscriptionName, DeadLetterReason, DeadLetterCount,
Severity = iff(DeadLetterCount > 100, "🔴 Critical", iff(DeadLetterCount > 10, "🟡 Warning", "🟢 Normal"))
Best Practices
| Practice | Why |
|---|---|
| Filter at the subscription level, not in handler code | Reduces cost, latency, and handler complexity |
Use subject hierarchy (e.g., /orders/{region}/{id}) | Enables efficient prefix/suffix filtering |
| Keep advanced filters under 5 per subscription | Complex filters add evaluation latency |
Use StringIn over multiple StringContains | More efficient for exact-match scenarios |
| Enable dead lettering on all production subscriptions | Prevents silent event loss |
Set appropriate event-ttl and max-delivery-attempts | Balance between reliability and stale event delivery |
| Use CloudEvents schema for new topics | Better interoperability and richer filtering on source |
| Test filters with known events before production | Catch logic errors early |
| Monitor filter efficiency with KQL | Identify over-broad or over-narrow filters |
| Implement idempotent handlers | Required when using OR-pattern with multiple subscriptions |
Common Pitfalls
1. Assuming OR logic between advanced filters
All advanced filters are AND. An event must match every filter. Use multiple subscriptions or StringIn/NumberIn for OR semantics.
2. Case sensitivity in subject filters
Subject filters are case-sensitive by default. "/Orders/US" won't match "/orders/us". Set isSubjectCaseSensitive: false if needed.
3. Filtering on missing fields
If an event doesn't have the field referenced in an advanced filter, the filter evaluates to false — the event is silently dropped. Use IsNotNull to guard against this.
4. Exceeding filter limits Maximum 25 advanced filters per subscription, 25 values per filter. Exceeding these returns a validation error at creation time.
5. Not enabling dead lettering Without dead lettering, events that exhaust retries are permanently lost. Always configure a dead letter destination for production workloads.
6. Overlapping filters delivering duplicates When using multiple subscriptions to the same endpoint (OR pattern), the handler receives the event multiple times. Track event IDs to deduplicate.
7. Using NumberGreaterThan on string fields
Type mismatches cause the filter to never match. Ensure the event payload field type matches the filter operator type.