Azure · lesson 16 of 19
Beginner
4Intermediate
13- 5 Azure Virtual Network Basics
- 6 Azure SQL Basics
- 7 Azure Functions Fundamentals
- 8 Bicep and ARM Templates Basics
- 9 Azure Monitor Basics
- 11 Azure Cosmos DB: Items and Partition Keys
- 12 Azure Cosmos DB: Queries
- 13 Azure Cosmos DB: ETags and Patch
- 14 Azure Service Bus: Queues and Settlement
- 15 Azure Service Bus: Dead-Lettering
- 16 Azure Service Bus: Topics and Filters
- 17 Azure Key Vault: Secrets and Versions
- 18 Azure Key Vault: Soft-Delete and Recovery
Also part of Azure
5- + Dead-Letter Queues: What Happens When Retries Keep Failing from Event-Driven Architecture
- + Idempotent Consumers: Surviving a Duplicate Delivery from Event-Driven Architecture
- + Envelope Encryption and Key Rotation Without Re-encrypting Data from Cryptography
- + Deployments and Rollouts from Kubernetes
- + Services and Networking from Kubernetes
Azure Service Bus: Topics and Filters
Publish one message to an Azure Service Bus topic and see it fan out to two subscriptions with the Python SDK against the official emulator — then find out a SQL filter matches application properties, not whatever's inside the message body.
By SkillByExample · Published
A Service Bus topic publishes once; every subscription on it gets its own full copy of
each message that matches its filter, independent of what any other subscription does with
its copy. This lesson continues against the same Service Bus Emulator image, with a different
Config.json declaring a topic instead of a queue.
One Publish, Filtered to Two Different Subscriptions
import json
from azure.servicebus import ServiceBusClient, ServiceBusMessage
CONN_STR = "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
with ServiceBusClient.from_connection_string(CONN_STR) as client:
with client.get_topic_sender("events") as sender:
sender.send_messages(ServiceBusMessage(
json.dumps({"id": "t50", "total": 50}),
application_properties={"total": 50},
))
sender.send_messages(ServiceBusMessage(
json.dumps({"id": "t150", "total": 150}),
application_properties={"total": 150},
))
with client.get_subscription_receiver("events", "all", max_wait_time=5) as receiver:
ids = []
for msg in receiver.receive_messages(max_message_count=5, max_wait_time=5):
ids.append(json.loads(str(msg))["id"])
receiver.complete_message(msg)
print("'all' subscription:", sorted(ids))
with client.get_subscription_receiver("events", "big", max_wait_time=5) as receiver:
ids = []
for msg in receiver.receive_messages(max_message_count=5, max_wait_time=5):
ids.append(json.loads(str(msg))["id"])
receiver.complete_message(msg)
print("'big' subscription:", sorted(ids))
{
"UserConfig": {
"Namespaces": [
{
"Name": "sbemulatorns",
"Queues": [],
"Topics": [
{
"Name": "events",
"Properties": {
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"RequiresDuplicateDetection": false
},
"Subscriptions": [
{
"Name": "all",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"LockDuration": "PT30S",
"MaxDeliveryCount": 3,
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"RequiresSession": false
},
"Rules": [
{
"Name": "all-rule",
"Properties": {
"FilterType": "Sql",
"SqlFilter": { "SqlExpression": "1=1" }
}
}
]
},
{
"Name": "big",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"LockDuration": "PT30S",
"MaxDeliveryCount": 3,
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"RequiresSession": false
},
"Rules": [
{
"Name": "big-rule",
"Properties": {
"FilterType": "Sql",
"SqlFilter": { "SqlExpression": "total > 100" }
}
}
]
}
]
}
]
}
],
"Logging": { "Type": "File" }
}
}
$ docker network create az-sb-net-16
d1baa99865df81567e27160654296b4cea949bddfa5b1dc4e33c818be18e6bd4
$ docker run -d --name az-sb-sql-16 --network az-sb-net-16 -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD='yourStrong(!)Password' mcr.microsoft.com/mssql/server:2025-CU10-ubuntu-24.04
feedda8080ff21f788afa97cf35e7d2371de07c5ff09a266015fad34187fec82
$ until docker logs az-sb-sql-16 2>&1 | grep -q 'SQL Server is now ready for client connections'; do sleep 1; done
$ docker run -d --name az-sb-emu-16 --network az-sb-net-16 -v "$(pwd)"/Config.json:/ServiceBus_Emulator/ConfigFiles/Config.json -e ACCEPT_EULA=Y -e SQL_SERVER=az-sb-sql-16 -e MSSQL_SA_PASSWORD='yourStrong(!)Password' mcr.microsoft.com/azure-messaging/servicebus-emulator:2.0.1
abbedaa910a044f4c383a9fb016eb179985d008d9230ae60a2744e32f8d92ce9
$ until docker logs az-sb-emu-16 2>&1 | grep -q 'Emulator Service is Successfully Up'; do sleep 1; done
$ docker run --rm --network container:az-sb-emu-16 -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-servicebus==7.15.0 2>&1 | tail -1 && python sb_topic.py"
[notice] To update, run: pip install --upgrade pip
'all' subscription: ['t150', 't50']
'big' subscription: ['t150']
Both messages carry their total twice: once inside the JSON body, once as the
application_properties value the SQL filter actually reads. The 'all' subscription’s
1=1 filter matches everything, so both land there. The 'big' subscription’s total > 100
filter only matches t150 — if total had been left out of application_properties and only
embedded in the body, 'big' would have matched nothing at all, silently, with no error on
either the publish or the filter evaluation side.
Practice
1. Send a message whose total lives only in the JSON body, with no application_properties, and confirm the big subscription never sees it.
import json
from azure.servicebus import ServiceBusClient, ServiceBusMessage
CONN_STR = "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
with ServiceBusClient.from_connection_string(CONN_STR) as client:
with client.get_topic_sender("events") as sender:
sender.send_messages(ServiceBusMessage(json.dumps({"id": "t999-body-only", "total": 999})))
with client.get_subscription_receiver("events", "big", max_wait_time=5) as receiver:
ids = [json.loads(str(m))["id"] for m in receiver.receive_messages(max_message_count=5, max_wait_time=5)]
print("'big' subscription saw:", ids)
with client.get_subscription_receiver("events", "all", max_wait_time=5) as receiver:
ids = []
for m in receiver.receive_messages(max_message_count=5, max_wait_time=5):
ids.append(json.loads(str(m))["id"])
receiver.complete_message(m)
print("'all' subscription saw:", ids)
$ docker run --rm --network container:az-sb-emu-16 -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-servicebus==7.15.0 2>&1 | tail -1 && python sb_topic_body_only.py"
[notice] To update, run: pip install --upgrade pip
'big' subscription saw: []
'all' subscription saw: ['t999-body-only']
999 > 100 is true, but the filter never sees 999 — it’s only in the JSON text of the body,
not in an application property named total — so 'big' gets nothing. 'all'’s 1=1 filter
doesn’t inspect any property at all, so it still receives this message regardless.
2. Send a message with application_properties that has no matching value at all for the big subscription's filter, and predict whether that's an error or a non-match.
import json
from azure.servicebus import ServiceBusClient, ServiceBusMessage
CONN_STR = "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
with ServiceBusClient.from_connection_string(CONN_STR) as client:
with client.get_topic_sender("events") as sender:
sender.send_messages(ServiceBusMessage(
json.dumps({"id": "t-no-total-property"}),
application_properties={"category": "misc"},
))
with client.get_subscription_receiver("events", "big", max_wait_time=5) as receiver:
ids = [json.loads(str(m))["id"] for m in receiver.receive_messages(max_message_count=5, max_wait_time=5)]
print("'big' subscription saw:", ids)
with client.get_subscription_receiver("events", "all", max_wait_time=5) as receiver:
ids = []
for m in receiver.receive_messages(max_message_count=5, max_wait_time=5):
ids.append(json.loads(str(m))["id"])
receiver.complete_message(m)
print("'all' subscription saw:", ids)
$ docker run --rm --network container:az-sb-emu-16 -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-servicebus==7.15.0 2>&1 | tail -1 && python sb_topic_missing_property.py"
[notice] To update, run: pip install --upgrade pip
'big' subscription saw: []
'all' subscription saw: ['t-no-total-property']
A total > 100 comparison against a message that has no total property at all evaluates to
unmatched, the same as any other non-match — not an error, and not treated as true or false by
default. The message is simply never delivered to 'big'.
3. Send a message with total exactly 100 and predict whether the strict > 100 filter matches it.
import json
from azure.servicebus import ServiceBusClient, ServiceBusMessage
CONN_STR = "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
with ServiceBusClient.from_connection_string(CONN_STR) as client:
with client.get_topic_sender("events") as sender:
sender.send_messages(ServiceBusMessage(
json.dumps({"id": "t100-exact"}),
application_properties={"total": 100},
))
with client.get_subscription_receiver("events", "big", max_wait_time=5) as receiver:
ids = [json.loads(str(m))["id"] for m in receiver.receive_messages(max_message_count=5, max_wait_time=5)]
print("'big' subscription (total exactly 100):", ids)
with client.get_subscription_receiver("events", "all", max_wait_time=5) as receiver:
for m in receiver.receive_messages(max_message_count=5, max_wait_time=5):
receiver.complete_message(m)
$ docker run --rm --network container:az-sb-emu-16 -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-servicebus==7.15.0 2>&1 | tail -1 && python sb_topic_boundary.py"
[notice] To update, run: pip install --upgrade pip
'big' subscription (total exactly 100): []
total > 100 is a strict inequality — 100 itself doesn’t satisfy it, so 'big' gets nothing.
The filter means exactly what its SQL text says; a value sitting right on the boundary is a
non-match unless the filter was written with >=.
That’s the data-plane core of Service Bus this track covers on a local emulator. Sessions, duplicate detection, and scheduled messages all run on this same emulator too, but aren’t covered here — each is its own topic with its own gotchas, not a small addition to queues, dead-lettering, or filters.
Cleanup
$ docker rm -f az-sb-emu-16 az-sb-sql-16
az-sb-emu-16
az-sb-sql-16
$ docker network rm az-sb-net-16
az-sb-net-16
Next: Azure Key Vault — storing secrets, their automatic versioning, and what reading a version that doesn’t exist actually raises.