Yesterday was Event Hubs, the high volume stream. Service Bus is the other messaging pillar: fewer messages, more guarantees. Transactions, ordering via sessions, dead lettering, duplicate detection, and scheduled delivery. If Event Hubs is for telemetry, Service Bus is for the messages where losing or double processing one actually costs money: orders, payments, provisioning commands.
Queues, Topics, and When Each
Queues are point to point: one logical consumer, competing instances for scale. Topics add fan out: publishers send once, each subscription gets its own copy, and SQL filters on subscriptions route messages by properties. The design smell to avoid is one giant topic for everything with filters doing all the work; group by bounded context so ownership and access control stay clean.
resource "azurerm_servicebus_namespace" "this" {
name = "sbns-orders-prod"
location = "westeurope"
resource_group_name = azurerm_resource_group.msg.name
sku = "Premium"
capacity = 1
premium_messaging_partitions = 1
public_network_access_enabled = false
local_auth_enabled = false
}
resource "azurerm_servicebus_topic" "orders" {
name = "orders"
namespace_id = azurerm_servicebus_namespace.this.id
requires_duplicate_detection = true
duplicate_detection_history_time_window = "PT30M"
support_ordering = true
}
resource "azurerm_servicebus_subscription" "billing" {
name = "billing"
topic_id = azurerm_servicebus_topic.orders.id
max_delivery_count = 5
lock_duration = "PT1M"
dead_lettering_on_message_expiration = true
requires_session = true
}
resource "azurerm_servicebus_subscription_rule" "high_value" {
name = "high-value"
subscription_id = azurerm_servicebus_subscription.billing.id
filter_type = "SqlFilter"
sql_filter = "amount > 1000 AND region = 'EU'"
}
Premium tier for production: dedicated capacity, predictable latency, VNet integration with private endpoints, and larger message sizes. local_auth_enabled false kills SAS keys and forces Entra auth with managed identities.
Ordering with Sessions
Competing consumers destroy ordering by design. When you need strict per entity order (all events for order 12345 in sequence), set the session ID to that entity key. Service Bus then guarantees a session is owned by one consumer at a time, giving you ordered processing per key with parallelism across keys. It is the same mental model as Event Hubs partition keys, but with locking semantics and per message settlement.
The Delivery Contract: PeekLock, Retries, Dead Letters
Always PeekLock, never ReceiveAndDelete for anything that matters. The consumer receives the message under a lock, processes, then completes it; a crash means the lock expires and the message redelivers, which again makes idempotency mandatory. After max_delivery_count failed attempts the message moves to the dead letter queue instead of poisoning the pipeline forever.
Here is my strong opinion: a dead letter queue nobody monitors is a silent data loss mechanism with extra steps. Every DLQ needs an alert on depth greater than zero, a dashboard, and a documented triage runbook: inspect the dead letter reason property, classify as poison message (fix producer, discard), transient dependency failure (resubmit), or handler bug (fix, redeploy, resubmit). Build the resubmit tool before you need it, a small Function that copies DLQ messages back to the main entity is thirty lines and saves your worst afternoon.
Patterns Worth Stealing
Duplicate detection with a client set MessageId turns “the producer retried and we charged twice” into a non event, within the history window. Scheduled messages replace an entire class of cron jobs: enqueue the reminder at the moment you know it should fire. Deferral handles out of order arrivals when a dependency is not ready. And for cross entity consistency, prefer the transactional outbox on the producer side over distributed transactions: write the business row and the outbound message in one database transaction, and let a relay publish to Service Bus. Every one of these is a built in feature or a small pattern, and together they are why Service Bus remains the default answer for enterprise messaging on Azure.
Cheers
Osama
Leave a comment