Service Bus Topics + Subscriptions
One-to-many messaging with filters. Topics, subscriptions, SQL and correlation filter rules, and the patterns that fan out a single AI event to multiple downstream workers.
What a topic adds over a queue
A queue has one logical reader; a topic can have many. When a sender publishes a message to a topic, every subscription on that topic gets its own copy. Each subscription has its own receiver, its own delivery count, its own DLQ.
And subscriptions can have filters. The βembedβ subscription sees only messages where kind = 'article-created'; the βauditβ subscription sees everything. Filters happen in Service Bus β the wrong subscriptions never see irrelevant messages.
For AI: when a single event (βuser uploaded a documentβ) needs to fan out to multiple workers (embed it, scan it for PII, kick off translation), a topic with subscriptions is exactly right.
The fan-out model
βββ Subscription "embed" β Embedder worker
Topic: doc-events βββββΌββ Subscription "scan-pii" β PII scanner worker
βββ Subscription "translate" β Translation worker
βββ Subscription "audit" β Audit pipeline (no filter)
When you publish { kind: "article-created", id: "art-42", lang: "en" }, all four subscriptions get a copy. Filters can drop the copy at subscription time so workers only see what they care about.
Filter rules
| Feature | Boolean filter (TrueFilter / FalseFilter) | SQL filter | Correlation filter |
|---|---|---|---|
| Syntax | No expression β true or false | SQL-92-like expression on properties | Equality on system + user properties |
| Power | All or nothing | Most expressive | Limited but very efficient |
| Performance | Free (no eval) | Slightly higher CPU per match | Optimised β broker indexes correlation properties |
| Best for | Audit subscription that takes everything, or a sub that's effectively disabled | Filtering by message body / multiple properties / ranges | Common single-property equality (event type, tenant) |
# SQL filter β only messages where kind = 'article-created' AND lang IN ('en','fr')
az servicebus topic subscription rule create \
--resource-group roo-prod --namespace-name roo-sb \
--topic-name doc-events --subscription-name embed \
--name embed-filter \
--filter-sql-expression "kind = 'article-created' AND lang IN ('en','fr')"
# Correlation filter β the fast path for single-property equality
az servicebus topic subscription rule create \
--resource-group roo-prod --namespace-name roo-sb \
--topic-name doc-events --subscription-name scan-pii \
--name pii-filter \
--correlation-filter '{"properties":{"kind":"article-created"}}'
When you create a subscription, the default rule is 1=1 (true filter, takes everything). To filter, you remove that default rule and add your own.
# Remove the default $Default rule
az servicebus topic subscription rule delete \
--resource-group roo-prod --namespace-name roo-sb \
--topic-name doc-events --subscription-name embed \
--name '$Default'
Senders set properties; subscribers read them
# Sender β set custom properties on the message
message = ServiceBusMessage(
body=json.dumps(payload),
application_properties={
"kind": "article-created",
"lang": "en",
"tenant": "tidewater",
},
)
await sender.send_messages(message)
These properties are what filters evaluate. The body itself is opaque to filters β filters only see envelope properties.
| Property type | What it is | Filter access |
|---|---|---|
| Application properties | Arbitrary key/value set by the sender | kind, lang β referenced as bare names in SQL filters |
| System properties | Set by Service Bus (MessageId, CorrelationId, ContentType, To, β¦) | Referenced as sys.<name> |
| Message body | The actual payload | NOT visible to filters |
Real-world example: Theo's compliance routing
Tidewater Health publishes one event per patient-record edit to a topic. Three subscriptions consume it differently:
- embed β
kind = 'record-edited' AND fhir_resource = 'Encounter'(only certain resource types feed RAG) - audit β no filter (every change must be logged)
- alert β
kind = 'record-edited' AND severity = 'critical'(paged alerts only for critical edits)
Three workers, three workloads, one source of truth. If Theo adds a fourth concern next quarter (e.g., dispatch to a pharmacy AI), itβs another subscription with its filter β no change to the publisher.
Forwarding β chain subscriptions
A subscription can auto-forward its messages to another queue or topic. Useful for:
- Fanning a topic into a queue your worker already consumes
- Routing high-volume subscriptions into Premium-tier targets for performance
- Splitting a single subscription across multiple downstream workers
az servicebus topic subscription create \
--resource-group roo-prod --namespace-name roo-sb \
--topic-name doc-events --name embed \
--forward-to embed-jobs # forwarding to a queue named 'embed-jobs'
Now embedβs messages land in the embed-jobs queue automatically, and your existing queue worker consumes them.
Sessions on topics
Subscriptions support the same session model as queues. If a topic is configured for sessions, all subscriptions inherit that requirement, and senders must set session_id on every message.
This is the right pattern when each subscriber needs FIFO within a session AND multiple subscribers care about the same events.
Topic vs queue vs Event Grid β quick decision
| Pattern | Use |
|---|---|
| Queue | One sender, one (logical) consumer, durable |
| Topic + subscriptions | One publisher, many consumers, broker-side filtering, durable |
| Event Grid | Many publishers, many subscribers, push delivery, schema standardised, retries |
Service Bus is pull-based and durable; Event Grid is push-based and event-routing-focused. We cover Event Grid in the next module.
Key terms
Knowledge check
Mira publishes one message per uploaded image to a topic. Three teams want to consume: an embedder (only images), a moderation team (only adult content flagged at upload), and an audit team (everything). Which structure fits best?
Theo wants the audit subscription to receive every message regardless of body content. The other two subscriptions filter on a `severity` property. What should the audit subscription do?
Lin's filter `kind = 'event' AND tenant = 'acme'` runs on a topic with millions of messages per day. Performance is poor. Which filter optimisation is the simplest?