Use when implementing reliable message processing with Azure Service Bus — choosing between queues and topics, configuring peek-lock settlement, handling dead-lettered messages, or enforcing ordered processing with sessions.
Scanned 9/9/2026
Install to Claude Code
npx -y skills add kid-sid/codex-spellbook --skill azure-service-bus --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Azure Service Bus?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kid-sid-azure-service-bus)More formats (shields.io, HTML) on the badges page.
---
name: azure-service-bus
description: Use when implementing reliable message processing with Azure Service Bus — choosing between queues and topics, configuring peek-lock settlement, handling dead-lettered messages, or enforcing ordered processing with sessions.
---
# Azure Service Bus
Production patterns for Azure Service Bus using the `azure-servicebus` Python SDK.
## When to Activate
- Writing code that imports `azure-servicebus` or `@azure/service-bus`
- Choosing between Service Bus queues, topics, and subscriptions
- Implementing reliable message processing with peek-lock and settlement
- Handling dead-lettered messages or poison message scenarios
- Ensuring ordered processing with Service Bus sessions
- Filtering messages per subscriber using SQL or correlation filters
- Scheduling messages for future delivery or implementing deferred processing
- Comparing Azure Service Bus against Azure Storage Queues or Event Hubs
## Authentication
```python
from azure.identity import DefaultAzureCredential
from azure.servicebus import ServiceBusClient
credential = DefaultAzureCredential()
client = ServiceBusClient(
fully_qualified_namespace="myns.servicebus.windows.net",
credential=credential,
)
# BAD: connection string embeds shared access key
client = ServiceBusClient.from_connection_string(
"Endpoint=sb://myns.servicebus.windows.net/;SharedAccessKeyName=..."
)
# GOOD: keyless via RBAC role (Azure Service Bus Data Sender / Receiver)
client = ServiceBusClient(
fully_qualified_namespace=os.environ["SERVICEBUS_NAMESPACE"],
credential=DefaultAzureCredential(),
)
```
Always use `DefaultAzureCredential` and assign the minimum RBAC role:
| Role | Permission |
|---|---|
| `Azure Service Bus Data Sender` | Send messages only |
| `Azure Service Bus Data Receiver` | Receive and settle messages only |
| `Azure Service Bus Data Owner` | Full control — use only for admin tooling |
## Queues vs Topics/Subscriptions
| Factor | Queue | Topic + Subscriptions |
|---|---|---|
| Consumers | Single consumer group | Multiple independent consumers |
| Fan-out | No | Yes — each subscription gets a copy |
| Filtering | No | Yes — SQL or correlation filter per subscription |
| Ordering | Sessions only | Sessions only |
| Use case | Task queue, work distribution | Event fan-out, pub/sub |
```
Queue: Producer → [Queue] → Consumer A
Topic: Producer → [Topic] → [Sub: orders-billing] → Billing Service
→ [Sub: orders-shipping] → Shipping Service
→ [Sub: orders-analytics] → Analytics Service
```
## Sending Messages
### Single Message
```python
from azure.servicebus import ServiceBusMessage
import json
def send_message(namespace: str, queue_name: str, body: dict, **props) -> None:
credential = DefaultAzureCredential()
with ServiceBusClient(namespace, credential) as client:
with client.get_queue_sender(queue_name) as sender:
message = ServiceBusMessage(
json.dumps(body),
subject=props.get("subject"),
correlation_id=props.get("correlation_id"),
message_id=props.get("message_id"), # deduplication key
time_to_live=props.get("ttl"), # timedelta
application_properties=props.get("properties", {}),
)
sender.send_messages(message)
```
### Batch Send
```python
def send_batch(namespace: str, queue_name: str, messages: list[dict]) -> None:
with ServiceBusClient(namespace, DefaultAzureCredential()) as client:
with client.get_queue_sender(queue_name) as sender:
batch = sender.create_message_batch()
for body in messages:
try:
batch.add_message(ServiceBusMessage(json.dumps(body)))
except ValueError:
# Batch full — send current batch and start a new one
sender.send_messages(batch)
batch = sender.create_message_batch()
batch.add_message(ServiceBusMessage(json.dumps(body)))
if len(batch):
sender.send_messages(batch)
```
### Scheduled Messages
```python
from datetime import datetime, timedelta, timezone
def schedule_message(namespace: str, queue_name: str, body: dict, delay: timedelta) -> int:
enqueue_at = datetime.now(timezone.utc) + delay
with ServiceBusClient(namespace, DefaultAzureCredential()) as client:
with client.get_queue_sender(queue_name) as sender:
seq_numbers = sender.schedule_messages(
ServiceBusMessage(json.dumps(body)),
enqueue_at,
)
return seq_numbers[0] # use to cancel with cancel_scheduled_messages()
```
### Sending to a Topic
```python
def publish_event(namespace: str, topic_name: str, event_type: str, payload: dict) -> None:
with ServiceBusClient(namespace, DefaultAzureCredential()) as client:
with client.get_topic_sender(topic_name) as sender:
sender.send_messages(ServiceBusMessage(
json.dumps(payload),
subject=event_type,
application_properties={"event_type": event_type},
))
```
## Receiving Messages
### Peek-Lock (Recommended)
Locks the message for processing; must be explicitly settled. Ensures at-least-once delivery.
```python
from azure.servicebus import ServiceBusReceiveMode
def process_queue(namespace: str, queue_name: str, handler, max_messages: int = 10) -> None:
with ServiceBusClient(namespace, DefaultAzureCredential()) as client:
with client.get_queue_receiver(
queue_name,
receive_mode=ServiceBusReceiveMode.PEEK_LOCK,
max_wait_time=5,
) as receiver:
for msg in receiver.receive_messages(max_message_count=max_messages):
try:
body = json.loads(str(msg))
handler(body)
receiver.complete_message(msg) # ack — removes from queue
except Exception as e:
if msg.delivery_count >= 3:
receiver.dead_letter_message( # send to DLQ
msg,
reason="MaxRetriesExceeded",
error_description=str(e),
)
else:
receiver.abandon_message(msg) # nack — requeues with backoff
```
### Settlement Methods
| Method | Effect | When to use |
|---|---|---|
| `complete_message` | Removes from queue | Processing succeeded |
| `abandon_message` | Returns to queue; increments `delivery_count` | Transient failure, will retry |
| `dead_letter_message` | Moves to DLQ with reason | Poison message, max retries exceeded |
| `defer_message` | Parks with sequence number for later retrieval | Out-of-order messages needing dependencies |
### Continuous Consumer (Long-Running)
```python
import threading
def start_consumer(namespace: str, queue_name: str, handler, stop_event: threading.Event) -> None:
with ServiceBusClient(namespace, DefaultAzureCredential()) as client:
with client.get_queue_receiver(queue_name, max_wait_time=5) as receiver:
while not stop_event.is_set():
messages = receiver.receive_messages(max_message_count=10, max_wait_time=5)
if not messages:
continue
for msg in messages:
try:
handler(json.loads(str(msg)))
receiver.complete_message(msg)
except Exception as e:
logger.exception("message_processing_failed", extra={"delivery_count": msg.delivery_count})
receiver.abandon_message(msg)
```
## Dead-Letter Queue
```python
def process_dlq(namespace: str, queue_name: str) -> list[dict]:
dlq_path = f"{queue_name}/$deadletterqueue"
dead_letters = []
with ServiceBusClient(namespace, DefaultAzureCredential()) as client:
with client.get_queue_receiver(
dlq_path,
receive_mode=ServiceBusReceiveMode.PEEK_LOCK,
) as receiver:
for msg in receiver.receive_messages(max_message_count=50):
dead_letters.append({
"body": json.loads(str(msg)),
"reason": msg.dead_letter_reason,
"description": msg.dead_letter_error_description,
"delivery_count": msg.delivery_count,
"enqueued_at": msg.enqueued_time_utc,
})
receiver.complete_message(msg) # drain DLQ after inspection
return dead_letters
```
Alert when DLQ message count (`ActiveMessageCount` on the DLQ entity) exceeds zero.
## Sessions (Ordered Processing)
Sessions guarantee FIFO ordering for messages with the same `session_id`. The queue/topic subscription must have `requires_session=True`.
```python
# Send with session ID (all messages for the same order stay ordered)
def send_with_session(namespace: str, queue_name: str, session_id: str, body: dict) -> None:
with ServiceBusClient(namespace, DefaultAzureCredential()) as client:
with client.get_queue_sender(queue_name) as sender:
sender.send_messages(ServiceBusMessage(
json.dumps(body),
session_id=session_id,
))
# Receive a specific session
def process_session(namespace: str, queue_name: str, session_id: str, handler) -> None:
with ServiceBusClient(namespace, DefaultAzureCredential()) as client:
with client.get_queue_receiver(
queue_name,
session_id=session_id,
) as receiver:
for msg in receiver.receive_messages(max_message_count=100):
handler(json.loads(str(msg)))
receiver.complete_message(msg)
# Accept the next available session (let Service Bus assign)
def process_next_session(namespace: str, queue_name: str, handler) -> None:
with ServiceBusClient(namespace, DefaultAzureCredential()) as client:
with client.get_queue_receiver(
queue_name,
session_id=NEXT_AVAILABLE_SESSION, # from azure.servicebus
) as receiver:
for msg in receiver.receive_messages(max_message_count=100, max_wait_time=10):
handler(json.loads(str(msg)))
receiver.complete_message(msg)
```
## Subscription Filters
```python
from azure.servicebus.management import ServiceBusAdministrationClient, SqlRuleFilter, CorrelationRuleFilter
admin = ServiceBusAdministrationClient(
fully_qualified_namespace="myns.servicebus.windows.net",
credential=DefaultAzureCredential(),
)
# SQL filter — route by message property
admin.create_rule(
topic_name="orders",
subscription_name="orders-eu",
rule_name="eu-only",
filter=SqlRuleFilter("Region = 'EU'"),
)
# Correlation filter — cheaper, matches on built-in or application properties
admin.create_rule(
topic_name="orders",
subscription_name="orders-priority",
rule_name="priority-orders",
filter=CorrelationRuleFilter(
subject="order.placed",
application_properties={"priority": "high"},
),
)
# Remove the default catch-all rule when adding selective filters
admin.delete_rule("orders", "orders-eu", "$Default")
```
| Filter type | Performance | Flexibility | Use when |
|---|---|---|---|
| `TrueRuleFilter` | Fast | None | Subscription receives everything (default) |
| `CorrelationRuleFilter` | Fastest | Subject, correlation ID, app properties | Property-based routing |
| `SqlRuleFilter` | Slower | Full SQL expression | Complex conditions across multiple properties |
| `FalseRuleFilter` | Fast | None | Disable subscription without deleting it |
## Retry Configuration
```python
from azure.servicebus import ServiceBusClient
from azure.core.pipeline.policies import RetryPolicy
# SDK-level retry (network/transient errors)
client = ServiceBusClient(
fully_qualified_namespace="myns.servicebus.windows.net",
credential=DefaultAzureCredential(),
retry_total=5,
retry_backoff_factor=1.5,
retry_backoff_max=30,
)
# Message-level retry: set max_delivery_count on the queue/subscription
# (default 10 — messages exceeding this are auto-dead-lettered by Service Bus)
admin.update_queue(
admin.get_queue("my-queue"),
max_delivery_count=5,
lock_duration=timedelta(minutes=2), # must process within this window
)
```
## Error Handling
```python
from azure.servicebus.exceptions import (
ServiceBusError,
ServiceBusConnectionError,
ServiceBusAuthorizationError,
MessageLockLostError,
SessionLockLostError,
MessageAlreadySettled,
)
def safe_receive(receiver, handler, msg) -> None:
try:
handler(json.loads(str(msg)))
receiver.complete_message(msg)
except MessageLockLostError:
# Lock expired before we could settle — message will reappear
logger.warning("lock_expired", message_id=msg.message_id)
except MessageAlreadySettled:
# Duplicate settle call — safe to ignore
pass
except ServiceBusAuthorizationError:
# RBAC role missing — fail fast, don't retry
raise
except ServiceBusConnectionError as e:
logger.error("connection_lost", error=str(e))
raise # outer loop should reconnect
except Exception as e:
logger.exception("handler_failed")
try:
receiver.abandon_message(msg)
except MessageAlreadySettled:
pass
```
## Cost Controls
| Lever | Impact | How |
|---|---|---|
| Tier (Basic vs Standard vs Premium) | High | Basic: queues only, no topics/sessions; Standard: topics + sessions; Premium: dedicated capacity, no throttling |
| Message size | Medium | Standard max 256 KB; Premium max 100 MB — compress large payloads before sending |
| Message TTL | Medium | Set queue/message TTL to avoid accumulating unprocessed messages that waste storage |
| Auto-delete on idle | Low | Set `auto_delete_on_idle` on dev/staging queues to clean up abandoned resources |
| Duplicate detection window | Low | Enable on idempotent queues; deduplicated messages don't count toward throughput billing |
> See also: `event-driven`, `azure`, `observability`
## Red Flags
- **Completing a message before the handler finishes** — settling with `complete_message()` before your handler returns means a crash loses the work with no retry opportunity; settle only after successful processing
- **No monitoring on the dead-letter queue** — DLQ messages represent silently accumulating failures; set an alert on DLQ message count and review DLQ contents after every deployment
- **Lock duration shorter than max processing time** — if the peek-lock expires before processing completes, the message becomes visible again and gets processed twice; set lock duration to 2–3× your p99 processing time
- **Regular receiver used with session-enabled queues** — a standard `ServiceBusReceiver` ignores session grouping and violates ordering guarantees; use `accept_next_session()` for session-aware delivery
- **Catch-all topic subscription with no filters** — a `TrueRuleFilter` subscription on a high-volume topic processes every message; use correlation or SQL filters to subscribe only to relevant message types
- **`ServiceBusClient` recreated per message** — each client creation opens a new AMQP connection; create the client once at startup and reuse it across all sends and receives
- **Abandoning messages immediately on transient errors** — abandoning re-enqueues the message for immediate retry, potentially creating a tight loop; use `defer()` or back off before abandoning on transient failures
## Checklist
- [ ] All clients use `DefaultAzureCredential` — no connection strings or SAS keys in code
- [ ] Minimum RBAC role assigned: `Data Sender` for producers, `Data Receiver` for consumers
- [ ] Receive mode is `PEEK_LOCK` for all reliable processing — not `RECEIVE_AND_DELETE`
- [ ] All messages explicitly settled: `complete`, `abandon`, or `dead_letter` — no silent drops
- [ ] `delivery_count` checked before settling; poison messages sent to DLQ with reason and description
- [ ] DLQ monitored — alert fires when `ActiveMessageCount` on `/$deadletterqueue` exceeds 0
- [ ] `max_delivery_count` set on queues/subscriptions (not left at default 10 without review)
- [ ] `lock_duration` set long enough to cover worst-case processing time
- [ ] Sessions used for entities requiring ordered or correlated processing
- [ ] Subscription `$Default` rule deleted when selective `CorrelationRuleFilter` or `SqlRuleFilter` is added
- [ ] SDK client reused across calls — not instantiated per message
- [ ] Message TTL set to prevent unbounded queue depth on abandoned consumers
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!