DEV Community

Cover image for Azure Service Bus: Managed Messaging with Queues and Topics
Rhuturaj Takle
Rhuturaj Takle

Posted on

Azure Service Bus: Managed Messaging with Queues and Topics

Azure Service Bus: Managed Messaging with Queues and Topics

A practical guide to Azure Service Bus — Microsoft's fully managed enterprise messaging service — covering queues vs. topics/subscriptions, message sessions, delivery guarantees, dead-lettering, .NET integration, and how it compares to RabbitMQ and Kafka.


Table of Contents

  1. Introduction
  2. Queues: Point-to-Point Messaging
  3. Topics and Subscriptions: Publish/Subscribe
  4. Message Sessions: Ordered, Stateful Processing
  5. Delivery Guarantees and the PeekLock Model
  6. Dead-Lettering
  7. Scheduled and Deferred Messages
  8. Duplicate Detection
  9. .NET Integration with Azure.Messaging.ServiceBus
  10. Transactions
  11. Auto-Forwarding and Topologies
  12. Pricing Tiers and Namespace Design
  13. Service Bus vs. RabbitMQ vs. Kafka vs. Event Grid
  14. Common Pitfalls
  15. Quick Reference Table
  16. Conclusion

Introduction

Azure Service Bus is Microsoft's fully managed enterprise messaging service — a cloud-native alternative to self-hosting RabbitMQ (covered in this series' companion guide) that provides queues, publish/subscribe topics, and a set of enterprise-messaging features (sessions, transactions, duplicate detection) without any broker infrastructure to provision, patch, or cluster yourself.

await using var client = new ServiceBusClient(connectionString, new DefaultAzureCredential());
await using var sender = client.CreateSender("order-processing");

var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(order))
{
    ContentType = "application/json",
    MessageId = order.Id.ToString()
};
await sender.SendMessageAsync(message);
Enter fullscreen mode Exit fullscreen mode

If you've read this series' RabbitMQ guide, much of Service Bus will feel conceptually familiar — the core ideas (queues, at-least-once delivery, dead-lettering) carry over directly. This guide focuses on where Service Bus differs, what it adds as a managed, enterprise-focused service, and how to use it well from .NET.


1. Queues: Point-to-Point Messaging

Basic queue send/receive

az servicebus queue create --resource-group my-rg --namespace-name my-servicebus --name order-processing
Enter fullscreen mode Exit fullscreen mode
await using var sender = client.CreateSender("order-processing");
await sender.SendMessageAsync(new ServiceBusMessage(orderJson));

await using var receiver = client.CreateReceiver("order-processing");
ServiceBusReceivedMessage received = await receiver.ReceiveMessageAsync();
await receiver.CompleteMessageAsync(received);
Enter fullscreen mode Exit fullscreen mode

A Service Bus queue is directly analogous to a RabbitMQ queue — a single logical destination where each message is delivered to and processed by exactly one consumer, giving the same competing-consumers scaling model covered in this series' RabbitMQ guide when multiple receiver instances share a queue.

Queues vs. RabbitMQ: no separate exchange concept

Unlike RabbitMQ's publisher-always-sends-to-an-exchange model (covered in the RabbitMQ guide), Service Bus queues are sent to directly by name — there's no separate routing layer for queues specifically; the equivalent of RabbitMQ's flexible exchange-based routing is what topics and subscriptions provide instead (Section 2).


2. Topics and Subscriptions: Publish/Subscribe

The core publish/subscribe model

az servicebus topic create --resource-group my-rg --namespace-name my-servicebus --name order-events
az servicebus topic subscription create --resource-group my-rg --namespace-name my-servicebus --topic-name order-events --name email-service
az servicebus topic subscription create --resource-group my-rg --namespace-name my-servicebus --topic-name order-events --name inventory-service
Enter fullscreen mode Exit fullscreen mode
Topic: order-events
  Subscription: email-service      ← receives its own independent copy of every matching message
  Subscription: inventory-service  ← receives its own independent copy of every matching message
Enter fullscreen mode Exit fullscreen mode

A topic accepts published messages, and any number of subscriptions attached to it each receive their own independent copy — this is Service Bus's equivalent of RabbitMQ's fanout exchange (Section 3 of the RabbitMQ guide), but with subscriptions as durable, independently-managed entities rather than requiring manually declared queues bound to an exchange.

Publishing to a topic

await using var sender = client.CreateSender("order-events");
await sender.SendMessageAsync(new ServiceBusMessage(orderCreatedEventJson));
Enter fullscreen mode Exit fullscreen mode

Consuming from a subscription

await using var receiver = client.CreateReceiver("order-events", subscriptionName: "email-service");
var message = await receiver.ReceiveMessageAsync();
Enter fullscreen mode Exit fullscreen mode

Each subscription behaves like its own independent queue — with its own message backlog, its own dead-letter queue (Section 5), and its own set of consumers — while all subscriptions on the same topic see the same published messages (subject to any filters, below), directly mirroring the multiple-independent-consumer-groups capability covered in this series' Kafka guide, just implemented via a different underlying mechanism.

Subscription filters: selective delivery, without a separate exchange type

await adminClient.CreateRuleAsync("order-events", "high-value-orders",
    new CreateRuleOptions("HighValueFilter", new SqlRuleFilter("Total > 500")));
Enter fullscreen mode Exit fullscreen mode
// Correlation filter — matching on specific message properties, generally faster than a SQL filter
await adminClient.CreateRuleAsync("order-events", "premium-orders",
    new CreateRuleOptions("PremiumFilter", new CorrelationRuleFilter { Subject = "premium" }));
Enter fullscreen mode Exit fullscreen mode

Rather than RabbitMQ's routing-key-and-exchange-type model (topic/direct/fanout/headers), Service Bus subscriptions apply rules — SQL-like filter expressions evaluated against message properties, or lighter-weight correlation filters — directly to each subscription, determining which published messages that specific subscription actually receives. A subscription with no explicit rule defaults to receiving every message (the fanout behavior); adding a filter narrows it to only matching messages, giving topic-exchange-like selective routing without needing a distinct exchange type to declare upfront.


3. Message Sessions: Ordered, Stateful Processing

The problem sessions solve

As covered in this series' Kafka guide, maintaining strict ordering for a group of related messages (all events for one order, one customer, one conversation) typically requires a partition-key-style mechanism — Service Bus's answer to this within its queue/topic model is sessions.

var message = new ServiceBusMessage(eventJson) { SessionId = orderId.ToString() };
await sender.SendMessageAsync(message);
Enter fullscreen mode Exit fullscreen mode
await using var sessionReceiver = await client.AcceptNextSessionAsync("order-events-queue");
var messages = await sessionReceiver.ReceiveMessagesAsync(maxMessages: 10);
// all messages received here share the same SessionId, and are delivered in the order they were sent
Enter fullscreen mode Exit fullscreen mode

Setting a SessionId on related messages guarantees they're delivered in order, to a single consumer at a time, for the duration of that session — conceptually similar to Kafka's per-key ordering within a partition, but implemented as an explicit, queue/topic-native feature rather than something achieved through partition key hashing.

Session state: carrying context across a related sequence of messages

await sessionReceiver.SetSessionStateAsync(BinaryData.FromString(JsonSerializer.Serialize(currentOrderState)));
var state = await sessionReceiver.GetSessionStateAsync();
Enter fullscreen mode Exit fullscreen mode

Sessions can also carry small amounts of arbitrary state, associated with the session itself rather than any individual message — useful for a consumer that needs to accumulate context across a multi-message sequence (assembling a multi-part order, tracking a multi-step workflow) without needing external storage for that intermediate state.

When to use sessions

Sessions are the right tool specifically when a group of related messages must be processed strictly in order, by one consumer, one at a time — for messages with no such ordering requirement, plain (non-sessioned) queues/subscriptions scale better, since sessions inherently limit a given session's messages to sequential processing by a single consumer, trading some parallelism for the ordering guarantee.


4. Delivery Guarantees and the PeekLock Model

PeekLock: Service Bus's equivalent of manual acknowledgment

await using var receiver = client.CreateReceiver("order-processing",
    new ServiceBusReceiverOptions { ReceiveMode = ServiceBusReceiveMode.PeekLock }); // the default

var message = await receiver.ReceiveMessageAsync();
try
{
    await ProcessOrderAsync(message);
    await receiver.CompleteMessageAsync(message); // acknowledges — message removed from the queue
}
catch
{
    await receiver.AbandonMessageAsync(message); // releases the lock, message becomes available again
}
Enter fullscreen mode Exit fullscreen mode

PeekLock mode (the default and recommended mode) locks a message for a configurable duration once delivered to a receiver — the message remains in the queue, invisible to other receivers, until the receiver explicitly completes it (removing it permanently), abandons it (releasing the lock for redelivery), or the lock simply expires (also triggering redelivery). This is functionally equivalent to RabbitMQ's manual acknowledgment model covered in this series' companion guide — the same at-least-once delivery semantics and the same need for idempotent message handling apply identically here.

ReceiveAndDelete: the equivalent of RabbitMQ's autoAck: true

new ServiceBusReceiverOptions { ReceiveMode = ServiceBusReceiveMode.ReceiveAndDelete }
Enter fullscreen mode Exit fullscreen mode

Messages are removed from the queue the instant they're delivered, before any processing occurs — faster, but risks silent message loss if the receiver crashes mid-processing, exactly the same trade-off covered for RabbitMQ's auto-ack mode. Appropriate only for genuinely low-stakes, loss-tolerant messages.

Lock renewal for long-running processing

await using var receiver = client.CreateReceiver("order-processing");
var message = await receiver.ReceiveMessageAsync();

// For processing that might exceed the default lock duration:
await using var lockRenewer = new AutoLockRenewer(); // or manually: await receiver.RenewMessageLockAsync(message);
Enter fullscreen mode Exit fullscreen mode

If processing a message takes longer than its lock duration (configurable per queue/subscription, commonly 30 seconds to a few minutes by default), the lock expires and the message becomes available for redelivery to another receiver — while the original receiver is still working on it, risking duplicate concurrent processing. For genuinely long-running processing, explicitly renewing the lock periodically (or using the SDK's automatic lock renewal helper) prevents this, extending the lock for as long as processing legitimately continues.


5. Dead-Lettering

Automatic dead-lettering, built directly into the queue/subscription

await using var receiver = client.CreateReceiver("order-processing");
// ... after MaxDeliveryCount is exceeded automatically, or explicitly:
await receiver.DeadLetterMessageAsync(message, deadLetterReason: "ValidationFailed");
Enter fullscreen mode Exit fullscreen mode
az servicebus queue update --resource-group my-rg --namespace-name my-servicebus --name order-processing --max-delivery-count 5
Enter fullscreen mode Exit fullscreen mode

Unlike RabbitMQ, where a dead letter exchange requires explicit configuration (a separate exchange, a binding, and a queue argument, as covered in this series' RabbitMQ guide), Service Bus queues and subscriptions have a dead-letter sub-queue built in automatically — every queue and subscription implicitly has one, and MaxDeliveryCount (a simple per-queue/subscription setting) automatically moves a message there once it's been delivered and abandoned/expired that many times, with no additional topology to declare.

Consuming from the dead-letter queue

await using var dlqReceiver = client.CreateReceiver("order-processing",
    new ServiceBusReceiverOptions { SubQueue = SubQueue.DeadLetter });

var deadLetteredMessage = await dlqReceiver.ReceiveMessageAsync();
Console.WriteLine($"Dead-lettered: {deadLetteredMessage.DeadLetterReason} - {deadLetteredMessage.DeadLetterErrorDescription}");
Enter fullscreen mode Exit fullscreen mode

The dead-letter sub-queue is addressed via the same queue/subscription name with a SubQueue.DeadLetter designation — genuinely simpler to set up than RabbitMQ's explicit DLX configuration, at the cost of somewhat less routing flexibility (RabbitMQ's DLX can route to an arbitrary exchange with its own routing logic; Service Bus's dead-letter queue is a fixed, built-in destination per queue/subscription).

Explicit dead-lettering with a reason

await receiver.DeadLetterMessageAsync(message,
    deadLetterReason: "InvalidOrderData",
    deadLetterErrorDescription: "Missing required CustomerId field");
Enter fullscreen mode Exit fullscreen mode

Beyond automatic dead-lettering after exceeding MaxDeliveryCount, application code can explicitly dead-letter a message it recognizes as unprocessable (a validation failure, a genuinely malformed payload) immediately, with a descriptive reason — avoiding wasted redelivery attempts for a message that's already known to be permanently unprocessable, and giving whoever investigates the dead-letter queue meaningful context rather than just a generic delivery-count-exceeded message.


6. Scheduled and Deferred Messages

Scheduling a message for future delivery

var message = new ServiceBusMessage(reminderJson);
await sender.ScheduleMessageAsync(message, DateTimeOffset.UtcNow.AddHours(24));
Enter fullscreen mode Exit fullscreen mode

Service Bus supports natively scheduling a message to become available only at a specified future time — directly useful for reminder notifications, delayed retry patterns (achieving with a single built-in feature what this series' RabbitMQ guide implements via a TTL-plus-dead-letter-exchange workaround), or any workflow needing a deliberate delay before a message should actually be processed.

Deferring a message for later, explicit retrieval

await receiver.DeferMessageAsync(message); // sets it aside, NOT visible to normal receive calls

// later, once ready to handle it, using the message's sequence number:
var deferredMessage = await receiver.ReceiveDeferredMessageAsync(message.SequenceNumber);
Enter fullscreen mode Exit fullscreen mode

Deferral is distinct from scheduling — a deferred message is explicitly set aside by the receiver (not automatically redelivered or made visible via normal receive calls) and can only be retrieved later by its specific sequence number, useful for out-of-order processing scenarios (e.g., "I've received message 3 of a 5-part sequence, but need to wait for messages 1 and 2 first") where a consumer needs to hold onto a message until some other condition is satisfied.


7. Duplicate Detection

Broker-side deduplication, based on MessageId

await using var sender = client.CreateSender("order-processing");
var message = new ServiceBusMessage(orderJson) { MessageId = order.Id.ToString() };
await sender.SendMessageAsync(message);
Enter fullscreen mode Exit fullscreen mode
az servicebus queue create --resource-group my-rg --namespace-name my-servicebus --name order-processing \
    --enable-duplicate-detection true --duplicate-detection-history-time-window "00:10:00"
Enter fullscreen mode Exit fullscreen mode

When enabled on a queue/topic, Service Bus tracks MessageId values within a configurable time window and automatically discards a message with a MessageId it's already seen within that window — this directly addresses the producer-side duplicate risk covered for Kafka's idempotent producer feature in this series' Kafka guide, but implemented broker-side and keyed on an application-supplied ID rather than Kafka's lower-level producer-retry deduplication. This is a genuinely convenient built-in mitigation for the common "my publisher retried after a network blip and sent the same message twice" scenario — though it's scoped to a time window, not indefinite, and doesn't replace the need for idempotent consumer-side processing (Section 4) as the more complete, defense-in-depth solution to at-least-once delivery's duplicate risk generally.


8. .NET Integration with Azure.Messaging.ServiceBus

Setup and dependency injection

builder.Services.AddSingleton(_ =>
    new ServiceBusClient(builder.Configuration["ServiceBus:Namespace"], new DefaultAzureCredential()));

builder.Services.AddSingleton(provider =>
    provider.GetRequiredService<ServiceBusClient>().CreateSender("order-processing"));
Enter fullscreen mode Exit fullscreen mode

ServiceBusClient (like ConnectionMultiplexer for Redis, covered in this series' Redis guide, and CosmosClient for Cosmos DB) is designed to be created once and shared for the application's lifetime — registered as a singleton, it manages connection pooling internally.

Authentication: Managed Identity as the recommended default

new ServiceBusClient(fullyQualifiedNamespace, new DefaultAzureCredential())
Enter fullscreen mode Exit fullscreen mode

As covered in this series' Secret Management guide, DefaultAzureCredential combined with Managed Identity is the recommended authentication approach — eliminating the need to store a Service Bus connection string (which embeds a shared access key, itself a genuine secret) at all, when running on Azure compute that supports Managed Identity.

Processing messages with ServiceBusProcessor

public class OrderProcessingWorker : BackgroundService
{
    private readonly ServiceBusProcessor _processor;

    public OrderProcessingWorker(ServiceBusClient client)
    {
        _processor = client.CreateProcessor("order-processing", new ServiceBusProcessorOptions
        {
            MaxConcurrentCalls = 10,
            AutoCompleteMessages = false
        });
        _processor.ProcessMessageAsync += HandleMessageAsync;
        _processor.ProcessErrorAsync += HandleErrorAsync;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _processor.StartProcessingAsync(stoppingToken);
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }

    private async Task HandleMessageAsync(ProcessMessageEventArgs args)
    {
        var order = JsonSerializer.Deserialize<Order>(args.Message.Body);
        await ProcessOrderAsync(order);
        await args.CompleteMessageAsync(args.Message);
    }

    private Task HandleErrorAsync(ProcessErrorEventArgs args)
    {
        _logger.LogError(args.Exception, "Service Bus processing error");
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

ServiceBusProcessor is a higher-level abstraction over manual receive loops — it manages a pool of concurrent message-processing calls (MaxConcurrentCalls), automatic lock renewal for long-running handlers, and structured error handling via events, letting a BackgroundService (following the same hosting pattern covered throughout this series) focus purely on the actual message-handling logic rather than the receive-loop mechanics.


9. Transactions

Atomic operations across multiple messaging actions

await using var transaction = new ServiceBusTransaction(); // conceptual — actual API uses ServiceBusClient's transaction support via TransactionScope

using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
await receiver.CompleteMessageAsync(incomingMessage);
await sender.SendMessageAsync(new ServiceBusMessage(outgoingEventJson));
scope.Complete();
Enter fullscreen mode Exit fullscreen mode

Service Bus supports participating in .NET's TransactionScope, letting a "complete this incoming message" and "send this outgoing message" pair succeed or fail together atomically — directly analogous to the Kafka transactions covered in this series' Kafka guide for "consume, process, produce" pipelines, giving the same all-or-nothing guarantee for a specific, common messaging pattern (receiving a message, taking some action, and publishing a resulting event) without needing to build that coordination manually.

Scope and limitations

As with Kafka's transactional guarantees, Service Bus transactions cover operations within Service Bus itself (and, via TransactionScope's broader .NET transaction coordination, potentially a local SQL Server operation in the same scope) — they don't extend atomicity to arbitrary external systems (an HTTP call to a third-party API, for instance), so the same idempotency discipline covered throughout this series' messaging guides remains necessary for anything touching systems outside the transaction's actual scope.


10. Auto-Forwarding and Topologies

Chaining queues and subscriptions without application code

az servicebus queue update --resource-group my-rg --namespace-name my-servicebus \
    --name order-processing --forward-to order-processing-archive
Enter fullscreen mode Exit fullscreen mode

Auto-forwarding lets a queue or subscription automatically forward every message it receives to another queue or topic — entirely broker-side, with no consumer application needed in between — useful for building multi-stage processing topologies (a subscription filtering high-value orders that auto-forwards into a separate, more heavily-monitored processing queue) or simple archival patterns, without writing and deploying a dedicated forwarding service.

Building fan-out-then-filter topologies

Topic: order-events
  Subscription: "high-value" (filter: Total > 1000) → auto-forwards to → Queue: fraud-review
  Subscription: "standard"    (filter: Total <= 1000) → consumed directly by the normal processing service
Enter fullscreen mode Exit fullscreen mode

Combining topic subscriptions' filtering (Section 2) with auto-forwarding lets fairly sophisticated routing topologies be expressed entirely as Service Bus configuration — declared once via the Azure CLI/Bicep/Terraform (connecting to this series' Terraform/Bicep guide), rather than requiring custom application code to inspect and re-route messages.


11. Pricing Tiers and Namespace Design

Basic, Standard, and Premium tiers

Tier Key differences
Basic Queues only, no topics/subscriptions, no sessions, pay-per-operation
Standard Full feature set (topics, sessions, transactions), pay-per-operation with a base charge
Premium Dedicated, predictable-performance resources (not shared multi-tenant capacity), larger message sizes, virtual network integration, higher throughput ceilings

The tier decision is mostly about which features are actually needed (topics require at least Standard) and, for Premium specifically, whether workload volume and latency-predictability requirements justify dedicated capacity over the shared, pay-per-operation model of Basic/Standard.

Namespace organization

Namespace: my-app-prod
  Queue: order-processing
  Queue: email-notifications
  Topic: order-events
    Subscription: inventory-service
    Subscription: analytics-service
Enter fullscreen mode Exit fullscreen mode

A Service Bus namespace is the top-level container (analogous to a RabbitMQ virtual host) — most applications use a small number of namespaces (often one per environment: dev/staging/production), with queues and topics organized underneath, rather than one namespace per individual queue, since namespace-level settings (network access rules, Managed Identity role assignments) are more naturally managed at that broader scope.


12. Service Bus vs. RabbitMQ vs. Kafka vs. Event Grid

Azure Service Bus RabbitMQ Kafka Azure Event Grid
Model Managed enterprise messaging (queues + topics) Self-hosted (or managed), exchange-based routing Distributed, retained, replayable log Managed, event-driven pub/sub for reactive architectures
Operational burden None — fully managed Self-hosted clustering, or a managed offering Highest — partition/broker/replication management None — fully managed
Ordering guarantees Via sessions Per-queue FIFO (no partition concept) Per-partition, via partition key No ordering guarantee
Message retention/replay Time-limited (dead-letter and deferral aside), not a replay-oriented model Consumed-and-removed Long-term retained, fully replayable Not retained — at-most-once-ish delivery for reactive triggers
Enterprise features Sessions, transactions, duplicate detection, auto-forwarding — built in Requires more manual assembly (DLX, TTL tricks) for equivalent behavior Requires external tooling (Schema Registry, Streams) for equivalent governance Minimal — designed for lightweight event routing, not durable processing
Best fit Azure-native enterprise applications wanting managed, feature-rich messaging Flexible routing, self-hosted or cross-cloud portability High-volume event streaming, replay, multiple independent consumers Lightweight reactive triggers (a blob uploaded, a resource changed)

Practical guidance

  • Building on Azure, want enterprise messaging features (sessions, transactions, dead-lettering) without operating broker infrastructure yourself? → Service Bus is the natural, purpose-built choice.
  • Need Kafka's replay/high-volume streaming capabilities specifically? → Service Bus isn't the right fit; use Kafka directly or Azure Event Hubs (Azure's Kafka-like managed streaming service, distinct from Service Bus).
  • Need RabbitMQ's exchange-type routing flexibility, or want to avoid cloud vendor lock-in? → RabbitMQ (self-hosted or via a managed RabbitMQ offering) remains the more portable choice.
  • Need lightweight, fire-and-forget event routing for reactive automation (not durable, guaranteed-delivery business messaging)? → Azure Event Grid is a different, lighter-weight tool, worth distinguishing from Service Bus specifically for that use case.

13. Common Pitfalls

Pitfall Why it hurts Better approach
Using ReceiveAndDelete mode for anything important Silent message loss if the receiver crashes mid-processing Use PeekLock (the default) and explicitly complete only after successful processing
Not renewing locks for long-running message processing The lock expires mid-processing, risking duplicate concurrent handling Use AutoLockRenewer or explicit periodic RenewMessageLockAsync calls
Forgetting sessions require a session-aware receiver A non-session receiver can't receive from a session-enabled queue at all Use AcceptNextSessionAsync/session-specific receivers when RequiresSession is enabled
Assuming duplicate detection alone guarantees exactly-once It's a time-windowed, MessageId-based mitigation, not a complete guarantee Still design consumers to be idempotent
Storing a Service Bus connection string (with an embedded shared access key) in application config A long-lived credential sitting in configuration, per this series' Secret Management guide Use Managed Identity with DefaultAzureCredential instead
Never checking the dead-letter queue Failed messages accumulate silently, with no visibility into recurring processing failures Monitor dead-letter queue depth; alert on sustained growth
Choosing Basic tier, then needing topics later Basic tier doesn't support topics/subscriptions at all — requires a tier migration Default to Standard tier unless queue-only, cost-sensitive use is clearly sufficient

Quick Reference Table

Concept Purpose
Queue Point-to-point, competing-consumers messaging
Topic + Subscription Publish/subscribe, each subscription an independent copy
Subscription filter/rule Selective delivery to a specific subscription, SQL-like or correlation-based
Session (SessionId) Ordered, single-consumer-at-a-time processing for related messages
PeekLock Manual acknowledgment mode; the recommended default
MaxDeliveryCount Automatic dead-lettering threshold, built in per queue/subscription
Scheduled message Native delayed delivery to a future time
Deferred message Explicitly set aside, retrieved later by sequence number
Duplicate detection Broker-side, MessageId-based, time-windowed deduplication
Auto-forwarding Broker-side chaining of queues/topics without application code
ServiceBusProcessor High-level .NET abstraction managing concurrent processing and lock renewal

Conclusion

Azure Service Bus takes the core messaging concepts covered in this series' RabbitMQ guide and packages them as a fully managed service with a distinctly enterprise-messaging feature set built in from the start — sessions for ordered processing, transactions spanning multiple messaging operations, duplicate detection, and dead-lettering that requires no manual topology configuration the way RabbitMQ's does. For teams building on Azure who want these capabilities without operating broker infrastructure themselves, it's a strong, purpose-built default.

The right choice among Service Bus, RabbitMQ, and Kafka — echoing this series' Kafka guide's conclusion — comes down to matching the tool to the actual problem shape: Service Bus for managed, feature-rich enterprise messaging within the Azure ecosystem; RabbitMQ for flexible routing with self-hosted or cross-cloud portability; Kafka for high-volume, replayable event streaming with many independent downstream consumers. All three share the same underlying disciplines this series has emphasized throughout — at-least-once delivery by default, idempotent consumers, deliberate dead-letter handling, and bounded retry policies — the specific mechanism for each just differs by platform.


Found this useful? Feel free to star the repo, open an issue with corrections, or share the session-based ordering fix that resolved a race condition RabbitMQ or Kafka would have needed a different trick to solve.

Top comments (1)

Collapse
 
swapnoneel123 profile image
Swapnoneel Saha

the delivery guarantee section covers the risks that matter in production. i would add a small failure matrix for lock expiry, handler retry, duplicate message ids, and dead letter recovery, with the required consumer behavior for each case. a trace id and message id in logs can connect the original delivery to later retries. this makes it easier to prove that a handler is safe under at least once delivery.