Event Filters and Retries

Earn 25 points (50 with Pro) in two steps

  1. ① Read through the lesson — each section gets a ✓ as you scroll through it.
  2. ② When every section has a ✓, tap Complete lesson.

0 of 8 read · keep scrolling

✦ See fewer ads and earn double points — 50 a lesson instead of 25 — with Pro

Lesson: Mastering Event Filters and Retries in Azure

Introduction: The Architecture of Reliable Event-Driven Systems

In modern cloud computing, distributed systems rely heavily on asynchronous communication. When services need to talk to one another without being tightly coupled, we turn to event-driven architectures. In the Azure ecosystem, this often involves services like Azure Event Grid, Service Bus, and Event Hubs. However, simply sending messages from point A to point B is rarely enough to build a professional-grade application. You must account for the reality that networks fail, services go offline, and not every event is relevant to every subscriber.

This is where event filtering and retry policies become essential. Event filtering allows you to reduce noise and lower costs by ensuring that your services only process the data they actually need. Retry policies, on the other hand, provide the resilience necessary to handle transient errors—those brief moments of instability that occur in any large-scale network. Without these mechanisms, your system would be overwhelmed by unnecessary data processing and prone to failure at the slightest hint of network latency.

Understanding how to implement filters and retries is the difference between an application that breaks under pressure and one that gracefully handles the unpredictability of the cloud. In this lesson, we will explore the technical implementation of these features, discuss the logic behind them, and learn how to configure them effectively within Azure.


Not read yet

Part 1: The Power of Event Filtering

Event filtering is the practice of inspecting incoming events and deciding whether to process them based on specific criteria. Without filters, a subscriber might receive thousands of events per second, only to discard 99% of them because they don't match the required criteria. This is inefficient, wastes compute resources, and increases your monthly bill.

Why Filtering Matters

When you use a service like Azure Event Grid, you can set up subscriptions for specific topics. However, a single topic might contain many different types of events. For instance, an "OrderManagement" topic might broadcast events for OrderCreated, OrderUpdated, OrderCancelled, and OrderShipped. If a shipping service only cares about OrderShipped events, filtering prevents that service from having to write code to ignore the other three types.

Types of Filtering in Azure Event Grid

Azure Event Grid provides several ways to filter events at the subscription level. By moving the filtering logic to the infrastructure layer, you offload the burden from your application code.

  • Subject Filtering: This allows you to filter based on the subject property of the event. You can use operators like beginsWith or endsWith to match specific paths or resource IDs.
  • Advanced Filtering: This is the most flexible method. It allows you to filter based on any property within the event data, such as the data payload itself. You can use operators like numberInRange, stringContains, stringIn, or boolEquals.
  • Event Type Filtering: This is the simplest form, where you provide a list of event types that the subscriber is interested in.

Practical Example: Implementing Advanced Filters

Imagine you have an inventory system that only wants to be notified when an item's stock level drops below 10. Rather than sending all inventory updates to a function and checking the value in code, you can define an advanced filter in your subscription.

{
  "properties": {
    "filter": {
      "advancedFilters": [
        {
          "operatorType": "NumberLessThan",
          "key": "data.stockLevel",
          "value": 10
        },
        {
          "operatorType": "StringIn",
          "key": "data.warehouseLocation",
          "value": ["EastUS", "WestUS"]
        }
      ]
    }
  }
}

In this JSON configuration, the subscriber will only receive events where the stockLevel is less than 10 AND the warehouseLocation is either "EastUS" or "WestUS". Any other event—such as a restock event or an update from a different warehouse—will be silently ignored by this subscription.

Callout: Filtering vs. Application Logic It is tempting to write filtering logic inside your application code (e.g., using if statements). However, doing this at the infrastructure level is almost always better. Infrastructure-level filtering saves you from paying for the execution of your function or logic app, reduces latency, and keeps your code clean and focused on business logic rather than message routing.


Not read yet

Part 2: Handling Failures with Retry Policies

Even with perfect filtering, your system will eventually encounter errors. A database might be busy, an API might be throttling your requests, or a transient network glitch might prevent a message from being delivered. A retry policy defines how your system should behave when a delivery attempt fails.

The Anatomy of a Retry Policy

When an event delivery fails, Azure services don't just give up immediately. They utilize a retry policy to attempt delivery again. A well-configured policy typically includes two main parameters:

  1. Max Delivery Attempts: The total number of times the system will try to send the event before giving up.
  2. Event Time-to-Live (TTL): The duration for which the event remains in the system while waiting to be delivered.

Exponential Backoff

A simple "retry every second" approach is often a bad idea. If a service is crashing because it is overloaded, hitting it repeatedly every second will only make the problem worse—a phenomenon known as the "thundering herd" problem. Instead, we use exponential backoff. This means the wait time between retries increases as the number of failures increases. For example:

  • Attempt 1: Fail. Wait 10 seconds.
  • Attempt 2: Fail. Wait 30 seconds.
  • Attempt 3: Fail. Wait 1 minute.
  • Attempt 4: Fail. Wait 5 minutes.

This gives the downstream service time to recover and clear its own queues before being hit with another request.

Configuring Retries in Azure Service Bus

Azure Service Bus handles retries differently than Event Grid, as it is a message-based system. You can control retries via the MaxDeliveryCount property on a queue or subscription. When a message is received but not "completed" (the process fails), the delivery count increments. Once it reaches the maximum, the message is automatically moved to a Dead-Letter Queue (DLQ).

// C# example using Azure.Messaging.ServiceBus
ServiceBusReceiverOptions options = new ServiceBusReceiverOptions
{
    ReceiveMode = ServiceBusReceiveMode.PeekLock
};

// When processing fails, we do NOT call CompleteMessageAsync.
// The message will automatically reappear in the queue after the lock duration.
// Once MaxDeliveryCount is hit, it moves to the DLQ.

Note: Always move failed messages to a Dead-Letter Queue (DLQ). Never simply discard a message that fails repeatedly. By moving it to a DLQ, you gain the ability to inspect the message, debug the issue, and potentially replay the message once the underlying problem is resolved.


Not read yet

Part 3: Best Practices for Event-Driven Resilience

Designing for resilience is about assuming that things will go wrong. When you design your event-driven systems, keep the following industry-standard practices in mind to ensure your architecture remains stable.

Idempotency is Mandatory

Because we are using retries, there is a high probability that your system will process the same event more than once. If your subscriber receives an event, processes it, but then fails to send a confirmation back to the broker, the broker will assume the delivery failed and send the event again. If your application is not idempotent, this will lead to duplicate data, incorrect totals, or duplicate emails being sent.

To make your code idempotent:

  • Use a unique Event ID as a primary key in your database.
  • Check if the ID already exists in your records before performing an action.
  • Design your operations to be "set" operations rather than "increment" operations where possible (e.g., "Set balance to $50" is safer than "Add $10 to balance").

Monitoring and Alerting

Filtering and retries are "silent" features. If your filters are too aggressive, you might stop receiving data without knowing why. If your retries are failing, your DLQ might be filling up. You must configure alerts in Azure Monitor to notify your team when:

  • The number of dropped events due to filtering exceeds a threshold.
  • The number of messages in a Dead-Letter Queue is greater than zero.
  • The latency of message processing exceeds your acceptable business limits.

Designing for Failure

Do not assume your downstream services will always be available. If you have a process that relies on three different APIs, and one of them is down, your entire process should not necessarily fail. Use the "Circuit Breaker" pattern, where if a service fails multiple times, you stop trying to call it for a period of time to allow it to recover.

Comparison: Event Grid vs. Service Bus Retries

Feature Azure Event Grid Azure Service Bus
Primary Use Case Reactive event routing Message queuing and load leveling
Retry Strategy Built-in exponential backoff Configurable via delivery count
Failure Handling Dead-lettering to Storage Dead-lettering to dedicated DLQ
Filter Capability Advanced filtering at subscription Filtering via SQL-like expressions
Message Persistence Ephemeral (delivered or dropped) Persistent until consumed

Not read yet

Part 4: Step-by-Step Implementation Guide

Let’s walk through a common scenario: you have an application that receives stock market price updates, and you only want to process updates for specific high-value stocks.

Step 1: Create a Subscription with Filters

Using the Azure CLI, you can create an Event Grid subscription that filters out all stocks except for those with a high "volatility" score. This prevents your function from being triggered by low-volatility updates.

az eventgrid event-subscription create \
  --name my-filtered-subscription \
  --source-resource-id /subscriptions/{id}/resourceGroups/{rg}/providers/Microsoft.EventGrid/topics/stocks \
  --endpoint {webhook-url} \
  --advanced-filter data.volatility NumberGreaterThan 5.0

Step 2: Implement Idempotent Logic in the Consumer

In your Azure Function (the consumer), ensure you check for message uniqueness.

[FunctionName("ProcessStockUpdate")]
public async Task Run([EventGridTrigger] EventGridEvent eventGridEvent)
{
    string eventId = eventGridEvent.Id;
    
    // Check if we have already processed this event ID
    if (await _database.ExistsAsync(eventId))
    {
        return; // Skip, already processed
    }

    // Perform business logic...
    await _database.SaveStockUpdate(eventGridEvent.Data);
    
    // Mark as processed
    await _database.MarkAsProcessed(eventId);
}

Step 3: Configure Dead-Lettering

If your function fails to reach the database, ensure your infrastructure is configured to send failed messages to a storage container. This allows you to perform an audit later.

  1. Navigate to your Event Grid Subscription in the Azure Portal.
  2. Select "Dead Lettering" under the settings menu.
  3. Choose a Storage Account and a container for the failed blobs.
  4. Enable the feature.

Not read yet

Part 5: Common Pitfalls and How to Avoid Them

Pitfall 1: Over-Filtering

A common mistake is creating filters that are so strict they accidentally exclude valid data. For example, if you filter by data.type == 'Purchase', but the source later adds a new event type called PurchaseV2, your system will stop receiving all purchases.

  • Avoidance: Use broad filters initially and refine them over time. Monitor your "dropped events" metrics closely to ensure you aren't filtering out data you need.

Pitfall 2: Infinite Retry Loops

If you don't set a MaxDeliveryCount, some systems might retry a message indefinitely. This creates a "poison message" scenario where a single corrupted message keeps crashing your processor.

  • Avoidance: Always set a finite number of retries (e.g., 5-10). If the message still fails, rely on the Dead-Letter Queue mechanism.

Pitfall 3: Ignoring Retries in Code

Some developers assume that because they have "retry policies" configured in the infrastructure, they don't need to handle retries in their code. This is false. Infrastructure retries handle delivery to your endpoint; they do not handle internal application logic failures.

  • Avoidance: Use libraries like Polly in C# or equivalent retry libraries in your language of choice to handle retries within your application logic when calling external APIs.

Warning: Do not confuse "Infrastructure Retries" with "Application Retries". If your Function App receives an event but then fails to call an external API, the Event Grid infrastructure thinks the delivery was a success (because your function returned a 200 OK). You must catch exceptions within your code and either retry the API call or throw an exception so that Event Grid knows to retry the delivery.


Not read yet

Part 6: Advanced Concepts - The Dead-Lettering Workflow

Dead-lettering is often misunderstood as a "trash bin" for data. In reality, it is a critical part of your operational workflow. When a message hits the DLQ, it is an indicator that your system has encountered a scenario it wasn't prepared for.

The DLQ Triage Process

  1. Alerting: Have an alert trigger as soon as a message hits the DLQ.
  2. Inspection: Use a tool (like Azure Storage Explorer for blobs or a custom script for Service Bus) to read the message.
  3. Correction: Determine if the failure was a transient issue (which has since resolved) or a data issue (a malformed message).
  4. Re-injection: If the issue is resolved, write a small script to move the message back into the main queue or topic for reprocessing.

By treating the DLQ as a "pending review" queue rather than a "failure dump," you turn a potential data loss event into an opportunity for system improvement.


Not read yet

Part 7: Key Takeaways for Your Architecture

As we conclude this lesson, remember that reliability is not a feature you turn on; it is a discipline you practice. Here are the core principles you should take away from this module:

  1. Filter Early: Use infrastructure-level filtering to reduce noise and costs. Only process the data that is essential for your business requirements.
  2. Always Expect Failure: Design your consumers to handle intermittent outages. If your system can't recover from a 5-second network blip, it isn't ready for production.
  3. Idempotency is Non-Negotiable: Because retries are a fundamental part of distributed systems, you must ensure that processing the same event twice does not result in corrupted data.
  4. Leverage Dead-Letter Queues: Never drop a message. If it cannot be processed, move it to a DLQ so it can be inspected and potentially reprocessed.
  5. Monitor Your Infrastructure: Use Azure Monitor to keep an eye on your filter rates and DLQ counts. If you aren't measuring it, you aren't managing it.
  6. Understand the Tools: Know the difference between Event Grid (reactive) and Service Bus (queued). Each has different retry behaviors and filtering capabilities that should dictate your architectural choices.
  7. Keep Retries Smart: Use exponential backoff to avoid overwhelming downstream services. Never retry a failing service at a constant, high-frequency rate.

By mastering these concepts, you shift your role from merely writing code to designing systems that are resilient, efficient, and capable of operating at the scale required by modern cloud applications. The combination of targeted filtering and robust retry logic creates a foundation upon which you can build complex, reliable event-driven services that serve your users effectively, regardless of the challenges they face.


Quick Reference: Common Filter Operators

Operator Use Case
NumberInRange Validating values within a specific range (e.g., price, age).
StringContains Checking for substrings in URLs, IDs, or descriptions.
StringIn Matching against a whitelist of valid categories or regions.
BoolEquals Filtering based on flags (e.g., isPublished = true).
NumberGreaterThan Threshold-based filtering (e.g., only high-value transactions).

FAQ: Common Questions

Q: If I use filters, will I still be charged for the events that are filtered out? A: In Azure Event Grid, you are charged for the events that are ingested by the system, not just those that are delivered to subscribers. However, filtering at the subscription level prevents unnecessary compute costs in your downstream applications, which is usually the much larger portion of the bill.

Q: How many times should I retry a message? A: There is no single "magic number." For most systems, a max delivery count of 5 to 10 is sufficient. If a message cannot be processed in 10 attempts, it is highly likely that there is a structural issue with the message itself or a long-standing issue with the downstream system that requires human intervention.

Q: Can I change my filter after the subscription is created? A: Yes, you can update your subscription filters at any time via the Azure Portal, CLI, or PowerShell. Changes take effect almost immediately for new events, but they do not retroactively apply to events already in the queue.

Q: What happens if my DLQ fills up? A: A DLQ is just like any other queue. If it reaches its size quota, it will stop accepting new messages. This is why it is critical to have an automated process or an alert system to clear out the DLQ regularly. Do not let your "safety net" become a bottleneck.

Not read yet

Each section gets a ✓ as you scroll through it. Tap the button to jump to the next one.