Azure · lesson 14 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: Queues and Settlement
Send and receive messages through an Azure Service Bus queue with the Python SDK against the official Service Bus Emulator in Docker, and see what complete_message actually does: a message a reader already received doesn't come back on the next receive.
By SkillByExample · Published
Azure Service Bus is a managed message broker: producers send messages to a queue, and
consumers receive and settle them — explicitly marking each one done, abandoned, or
rejected. This lesson runs against Microsoft’s own Service Bus Emulator in Docker, which
needs a SQL Server container alongside it and a Config.json declaring the queue up front.
Sending Three Messages and Completing Them
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_queue_sender("orders") as sender:
for order_id in ["order-0", "order-1", "order-2"]:
sender.send_messages(ServiceBusMessage(order_id))
print("sent 3")
with client.get_queue_receiver("orders", max_wait_time=5) as receiver:
for msg in receiver.receive_messages(max_message_count=3, max_wait_time=5):
print("got", str(msg), "delivery", msg.delivery_count)
receiver.complete_message(msg)
with client.get_queue_receiver("orders", max_wait_time=3) as receiver:
remaining = receiver.receive_messages(max_message_count=3, max_wait_time=3)
print("after completing all three, remaining:", len(remaining))
{
"UserConfig": {
"Namespaces": [
{
"Name": "sbemulatorns",
"Queues": [
{
"Name": "orders",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT30S",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": false,
"RequiresSession": false
}
}
],
"Topics": []
}
],
"Logging": { "Type": "File" }
}
}
$ docker network create az-sb-net-14
def447444ad2a831f729664f07e6239558762731443d3b1052439c0a05ec098b
$ docker run -d --name az-sb-sql-14 --network az-sb-net-14 -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD='yourStrong(!)Password' mcr.microsoft.com/mssql/server:2025-CU10-ubuntu-24.04
1d767c4b2ee73368855c85055aba0e1cd046799d118b2d953d649dd647ac8e57
$ until docker logs az-sb-sql-14 2>&1 | grep -q 'SQL Server is now ready for client connections'; do sleep 1; done
$ docker run -d --name az-sb-emu-14 --network az-sb-net-14 -v "$(pwd)"/Config.json:/ServiceBus_Emulator/ConfigFiles/Config.json -e ACCEPT_EULA=Y -e SQL_SERVER=az-sb-sql-14 -e MSSQL_SA_PASSWORD='yourStrong(!)Password' mcr.microsoft.com/azure-messaging/servicebus-emulator:2.0.1
b352b338a39a88d0df7a94f3867b41b73d00c217397d88adb9b97b2c1430e6b0
$ until docker logs az-sb-emu-14 2>&1 | grep -q 'Emulator Service is Successfully Up'; do sleep 1; done
$ docker run --rm --network container:az-sb-emu-14 -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_queue.py"
[notice] To update, run: pip install --upgrade pip
sent 3
got order-0 delivery 0
got order-1 delivery 0
got order-2 delivery 0
after completing all three, remaining: 0
The emulator needs two containers and a mounted Config.json just to provision one queue — a
real namespace’s queues already exist and this setup step disappears entirely. delivery is 0
on this first-ever receive of each message, not 1; it counts redeliveries, so a message read
for the first time hasn’t been redelivered yet. (That 0 is specific to how the Python SDK
exposes the underlying AMQP delivery-count header — the .NET SDK and the Azure portal report
the same first delivery as 1.) After complete_message runs for all three, the queue is
empty — remaining comes back as 0, not because the messages are still there and locked, but
because they’re actually gone.
Peek-lock receive is why Service Bus delivers at least once, not exactly once: a message
stays in the queue, locked but not removed, until something explicitly completes it — so a
consumer that crashes after processing a message but before calling complete_message sees that
same message again once the lock expires. Code that isn’t naturally safe to run twice on the
same message (charging a card, incrementing a counter) needs to de-duplicate itself; see
idempotent consumers for the general pattern.
Practice
1. Receive a message and let the Python with block exit without calling complete_message, then check whether it comes back.
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_queue_sender("orders") as sender:
sender.send_messages(ServiceBusMessage("order-unsettled"))
with client.get_queue_receiver("orders", max_wait_time=5) as receiver:
msgs = receiver.receive_messages(max_message_count=1, max_wait_time=5)
for m in msgs:
print("received, not settled:", str(m), "delivery_count:", m.delivery_count)
# no complete_message call — the lock on this message is left to expire
with client.get_queue_receiver("orders", max_wait_time=35) as receiver:
again = receiver.receive_messages(max_message_count=1, max_wait_time=35)
for m in again:
print("received again after lock expiry:", str(m), "delivery_count:", m.delivery_count)
receiver.complete_message(m)
$ docker run --rm --network container:az-sb-emu-14 -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_no_settle.py"
[notice] To update, run: pip install --upgrade pip
received, not settled: order-unsettled delivery_count: 0
received again after lock expiry: order-unsettled delivery_count: 1
The message comes back on the second receiver because the first one never settled it — Config.json
set this queue’s LockDuration to PT30S (30 seconds), so the message becomes visible to another
receiver again once that lock expires, rather than staying invisible forever because one consumer
forgot to call complete_message.
2. Call abandon_message explicitly instead of waiting for the lock to expire, and confirm the message is available again immediately.
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_queue_sender("orders") as sender:
sender.send_messages(ServiceBusMessage("order-abandon-me"))
with client.get_queue_receiver("orders", max_wait_time=5) as receiver:
msgs = receiver.receive_messages(max_message_count=1, max_wait_time=5)
for m in msgs:
print("delivery count before abandon:", m.delivery_count)
receiver.abandon_message(m)
with client.get_queue_receiver("orders", max_wait_time=5) as receiver:
again = receiver.receive_messages(max_message_count=1, max_wait_time=5)
for m in again:
print("delivery count after abandon+redelivery:", m.delivery_count)
receiver.complete_message(m)
$ docker run --rm --network container:az-sb-emu-14 -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_abandon.py"
[notice] To update, run: pip install --upgrade pip
delivery count before abandon: 0
delivery count after abandon+redelivery: 1
abandon_message releases the lock immediately instead of waiting out LockDuration — the
message is available to the very next receive_messages call. The delivery count, which the
lock-expiry version of this also increments, is the queue’s own tally of how many times this
message has been handed to a receiver, independent of which receiver instance did the receiving.
3. Peek a message without receiving it, then confirm a real receive still gets it.
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_queue_sender("orders") as sender:
sender.send_messages(ServiceBusMessage("order-peek-me"))
with client.get_queue_receiver("orders", max_wait_time=5) as receiver:
peeked = receiver.peek_messages(max_message_count=1)
for m in peeked:
print("peeked:", str(m), "delivery_count:", m.delivery_count)
with client.get_queue_receiver("orders", max_wait_time=5) as receiver:
received = receiver.receive_messages(max_message_count=1, max_wait_time=5)
for m in received:
print("still receivable after peek:", str(m), "delivery_count:", m.delivery_count)
receiver.complete_message(m)
$ docker run --rm --network container:az-sb-emu-14 -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_peek.py"
[notice] To update, run: pip install --upgrade pip
peeked: order-peek-me delivery_count: 0
still receivable after peek: order-peek-me delivery_count: 0
peek_messages is a read with no side effect on the message at all — no lock taken, no
delivery count bumped, nothing to abandon or complete afterward. It’s for inspecting a queue
without affecting anything a real consumer will later receive and settle, which is exactly why
the same message is still there, fully receivable, right after being peeked.
Cleanup
Each lesson in this Service Bus sequence starts its own fresh SQL Server and emulator pair — on a machine running through all of them without cleaning up, that’s a SQL Server and an emulator container left running per lesson. Remove this lesson’s containers and network before moving on:
$ docker rm -f az-sb-emu-14 az-sb-sql-14
az-sb-emu-14
az-sb-sql-14
$ docker network rm az-sb-net-14
az-sb-net-14
Next: dead-lettering — what happens to a message a consumer actively rejects.