In a distributed system where multiple microservices communicate via a shared Azure Service Bus topic, managing message routing efficiently becomes critical. Not every service needs to process every event, and activating services unnecessarily can lead to wasted compute and memory resources. This is especially true when each service only cares about a specific subset of events. Fortunately, Azure Service Bus provides a mechanism for filtering messages at the subscription level through subscription rules.

This article explores how to use SQL filters to restrict service activation only to relevant events, ensuring a more efficient message processing pipeline.

Problem: Overhead from Unfiltered Message Delivery

In my architecture, several microservices publish and subscribe to the same Azure Service Bus topic. The system includes an intake service that accepts requests via a REST API. This service processes those requests and emits events onto the shared topic. Downstream services subscribe to those events, perform necessary work, and send back status updates. The intake service, in turn, listens for these status updates to build a real-time manifest of all active tasks, which it exposes through the REST API.

The challenge arises because all services subscribe to the same topic, and without filtering, every service receives every event — relevant or not. While services can choose to ignore unrelated events at runtime, this still incurs a performance cost. Each message still triggers the message handler, consuming CPU and memory just to discard irrelevant messages.

To avoid this inefficiency, we can take advantage of Service Bus subscription filters to route only the relevant events to each subscriber.

Solution: SQL Filters on Service Bus Subscriptions

Azure Service Bus supports two types of subscription filters:

  • Correlation filters: Lightweight and fast, but limited to matching a single value on system properties like Subject, MessageId, etc.
  • SQL filters: More flexible, allowing for conditional logic based on both system and custom application properties.

In my case, the events we want to filter are distinguished by their Subject value, which identifies the event type, such as “Task.Completed” or “Task.InProgress”. Since this property is available on the message and we want to match multiple values, a SQL filter is the right tool.

Here’s how we define a SQL filter using Terraform for two different scenarios.

Example 1: Subscriber Interested in a Single Event Type

One microservice cares only about “Task.Created” events. The filter is applied at the subscription level like this:

resource "azurerm_servicebus_subscription_rule" "filter" {
  name            = "sbts-${var.application_name}-tasks-rule"
  subscription_id = azurerm_servicebus_subscription.tasks.id
  filter_type     = "SqlFilter"
  sql_filter      = "sys.Label IN ('Task.Created')"
}

This rule ensures that only messages with Subject = ‘Task.Created’ are delivered to the subscription. All other messages are ignored at the broker level and never reach the subscriber.

Example 2: Intake Service Interested in Multiple Status Events

The intake service needs to track the progress of all activities it initiated. It listens for several types of status updates, such as:

  • Task.InProgress
  • Task.Failed
  • Task.Completed

To capture all these events, the filter is written as follows:

resource "azurerm_servicebus_subscription_rule" "filter" {
  name            = "sbts-${var.application_name}-tasks-rule"
  subscription_id = azurerm_servicebus_subscription.tasks.id
  filter_type     = "SqlFilter"
  sql_filter      = "sys.Label IN ('Task.InProgress', 'Task.Failed', 'Task.Completed')"
}

With this rule in place, only the relevant messages trigger the intake service, minimizing resource consumption and ensuring accurate tracking of task status.

Optimizing for Maintainability

To make the configuration even cleaner and more maintainable, I refactored the SQL filter logic using Terraform localswith a for expression. This approach abstracts the list of events into a reusable local variable and dynamically constructs the SQL IN clause.

First, I defined a list of event names under local.servicebus_events, then used a for expression with format() to quote each event string. These are joined into a single comma-separated string, which is wrapped inside the final Subject IN (…) clause. Then the resulting SQL filter is stored in local.servicebus_events_string, which is then used directly in the azurerm_servicebus_subscription_ruleresource using string interpolation.

This setup not only keeps the rule definition concise but also makes it easy to update or reuse the event list across different modules or environments without repeating the filter logic.

Here’s the updated configuration:

locals {
  servicebus_events = [
    "Task.InProgress",
    "Task.Failed",
    "Task.Completed"
  ]
  servicebus_events_string     = join(", ", [for event in local.servicebus_events : format("'%s'", event)])
}

resource "azurerm_servicebus_subscription_rule" "filter" {
  name            = "sbts-${var.application_name}-tasks-rule"
  subscription_id = azurerm_servicebus_subscription.tasks.id
  filter_type     = "SqlFilter"
  sql_filter      = "sys.Label IN (${local.servicebus_events_string})"
}

This technique is especially helpful in larger systems where multiple services may each require slightly different filters. By externalizing the event list, the logic remains DRY (Don’t Repeat Yourself), easier to test, and easier to maintain. Our future selves don’t have to stumble around in a complex string, we can simply add, update, remove items from an array, helping us more easily identify typos before they break production!

Conclusion

Using SQL filters in Azure Service Bus allows microservices to subscribe only to the messages they care about, reducing unnecessary message handling and optimizing resource usage. This is particularly valuable in event-driven architectures where a shared topic is used across many services.

By leveraging SQL filters, services avoid the overhead of processing irrelevant events and can focus solely on their domain-specific responsibilities.

To further enhance maintainability and clarity, the filtering logic can be abstracted using Terraform locals and for expressions. This approach simplifies the filter definition, reduces repetition, and makes it easier to manage evolving sets of event types. It also promotes consistency across services and environments, turning what could be verbose configuration into clean, reusable, and maintainable infrastructure code.