Skip to content

Azure Service Bus & Event Hubs

Key Points

  • Service Bus = enterprise messaging. Queues + Topics. Sessions, DLQ, scheduled delivery, transactions, duplicate detection. For business commands/events.
  • Event Hubs = log-based event ingestion. Kafka-compatible. Partitioned; high-throughput (millions/s); replay. For telemetry, IoT, streams.
  • Decision: business workflow = Service Bus. Telemetry / event sourcing / streams = Event Hubs.
  • .NET SDK: Azure.Messaging.ServiceBus, Azure.Messaging.EventHubs. Both async-first; managed identity; OTel built-in.
  • See Brokers Comparison for full broker landscape.

Concepts (deep dive)

What Service Bus and Event Hubs are (and why they aren't the same thing)

Both are "message brokers" on Azure, but they solve fundamentally different problems with fundamentally different internals.

Service Bus is enterprise messaging: each message is a discrete unit of work (an order, a payment, a customer command) that one consumer should process exactly once, and the broker takes responsibility for delivery — peek-lock, abandon, complete, dead-letter, retry. It's the modern descendant of MSMQ/JMS/RabbitMQ — built for commands and events that drive business workflows. Messages live in queues or topics, are small (KBs), and consumers compete for them.

Event Hubs is a managed Kafka: an append-only partitioned log. Producers fire-and-forget billions of events per second; consumers read from offsets they manage themselves; events stay in the log for a retention window (1–90 days). It's built for telemetry, IoT, click streams, event sourcing — high volume, no per-message ack, replay is normal.

   Service Bus (messaging)                  Event Hubs (streaming)
   ────────────────────────                 ──────────────────────
   producer ──► queue ──► consumer          producer ──► partition 0 ──┐
                  │       (locks msg)       producer ──► partition 1 ──┤
                  │       complete/abandon  producer ──► partition 2 ──┤
                  │       MaxDeliveryCount                              │
                  └► DLQ if poison                                       │
                                            consumer reads from offset; │
   1 message ≈ 1 unit of work               other consumers read same   │
   broker tracks who got what               log independently            │
   topics/subscriptions for fan-out         retention window: 1–90 days │
   sessions for ordering                    replay = move offset back    │
   transactions across queues                                            │

Picking between them is the right starting question, and "I need pub/sub" doesn't decide it. Ask: is each message a thing to do (Service Bus) or a thing that happened (Event Hubs)? Does it matter if I process the same message twice (Service Bus offers dedup) or does my consumer dedupe by offset (Event Hubs)? Do I expect to replay history (Event Hubs natively; Service Bus uses scheduling/dead-letter-replay)?

Service Bus topology

Queue: producer → queue → consumer (1:1)
Topic: producer → topic → subscriptions → consumers (fan-out)
// Send
var client = new ServiceBusClient(ns, new DefaultAzureCredential());
var sender = client.CreateSender("orders");
await sender.SendMessageAsync(new ServiceBusMessage(json) { MessageId = id });

// Receive
var receiver = client.CreateReceiver("orders");
var msg = await receiver.ReceiveMessageAsync();
await receiver.CompleteMessageAsync(msg);

Service Bus tiers

Tier Notes
Basic Queues only; cheap
Standard Topics; sessions; transactions
Premium Reserved capacity; geo-DR; private link; large messages

Sessions (ordering)

Plain queue delivery is unordered — multiple consumers compete for messages, and a fast consumer can finish a later message before a slow consumer finishes an earlier one. Sessions are Service Bus's answer to "preserve order within a logical stream": tag related messages with the same SessionId, and the broker guarantees they're delivered to a single consumer in FIFO order, even with many consumers running in parallel. Other sessions go to other consumers — order is per-session, throughput is per-session-count.

await sender.SendMessageAsync(new ServiceBusMessage(json) { SessionId = customerId });

var sessionReceiver = await client.AcceptNextSessionAsync("orders");
await foreach (var msg in sessionReceiver.ReceiveMessagesAsync())
{
    Process(msg);
    await sessionReceiver.CompleteMessageAsync(msg);
}

Same SessionId always to same consumer; ordered. Crucial for per-entity ordering.

Dead-Letter Queue (DLQ)

A DLQ is the broker's escape hatch for poison messages — messages that have failed processing repeatedly. Without it, a bad message would be retried forever, blocking the queue behind it (head-of-line blocking). With it, after MaxDeliveryCount retries the broker quietly moves the message to a parallel queue you can inspect, fix, and resubmit. Service Bus gives every queue and subscription a built-in DLQ — no separate setup, no separate billing.

Queue / Topic-Subscription has a built-in DLQ.
Messages auto-DLQ after MaxDeliveryCount or explicit DeadLetter call.
var dlqReceiver = client.CreateReceiver("orders", new ServiceBusReceiverOptions
{
    SubQueue = SubQueue.DeadLetter
});

Inspect; fix; replay.

Scheduled messages

await sender.ScheduleMessageAsync(new ServiceBusMessage(json), DateTimeOffset.UtcNow.AddHours(1));

Native scheduling; no Quartz needed for this.

Duplicate detection

Networks lose acks; producers retry on timeout; the broker happily accepts the same message twice. Duplicate detection moves the idempotency problem from "every consumer rewrite" to "configure the queue once": Service Bus keeps a sliding window of recent MessageId values and silently drops duplicates within the window. The producer just needs to pick a deterministic MessageId (the business event ID, not a Guid.NewGuid() per retry).

Enabled at queue creation. MessageId = dedup key over a configurable window.

Send same MessageId twice → second is silently dropped. Idempotency at the broker.

Transactions

using var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
await sender.SendMessageAsync(msg1);
await sender.SendMessageAsync(msg2);
tx.Complete();   // both or neither

Within Service Bus only — not across DB. For cross-resource, see Outbox Pattern.

Premium features

  • Geo-DR: paired namespace; failover.
  • Private endpoints: VNet-only.
  • Large messages: up to 100 MB.

Event Hubs

Producers fire EventData batches at a hub; the hub appends them to a partition and forgets about delivery — there's no per-message ack to track. Consumers run as EventProcessorClient instances coordinated through a shared checkpoint store (a Blob container), each owning a slice of the partitions; the checkpoint tracks "I have read up to offset N on this partition." If a consumer crashes, another picks up its partitions from the last checkpoint.

var producer = new EventHubProducerClient(connStr, "telemetry");
using var batch = await producer.CreateBatchAsync();
batch.TryAdd(new EventData(json));
await producer.SendAsync(batch);

// Consumer (with checkpointing via Blob)
var processor = new EventProcessorClient(blobContainer, "consumerGroup", connStr, "telemetry");
processor.ProcessEventAsync += async args => { Process(args.Data); await args.UpdateCheckpointAsync(); };
processor.ProcessErrorAsync += args => { Log(args.Exception); return Task.CompletedTask; };
await processor.StartProcessingAsync();

Event Hubs tiers

Tier Notes
Basic 1 consumer group; throughput limit
Standard 20 consumer groups; capture
Premium / Dedicated Reserved capacity; tenant isolation

Partitions

A partition is an independent append-only log inside the hub — and the unit of ordering and parallelism. Events with the same partitionKey always land in the same partition and therefore arrive in order; events with different keys may land in different partitions and lose cross-key ordering. More partitions = more parallel consumers = more throughput, but also = harder to change later (partition count is set at creation and difficult to grow).

Event Hub: 1-32+ partitions. Same partition key = same partition = ordered.
Consumer groups: independent readers across same data.

Choose partition count for parallelism. Can't easily change after creation.

Capture

Auto-archive events to Blob / Data Lake. Cheap event sourcing.

Kafka-compatible API

hostname:port  → namespace:9093 (with SASL/SSL)

Use Confluent.Kafka client unchanged. Easy migration.

When Service Bus

  • Business events with workflow (order placed, payment processed).
  • Need DLQ + retries + sessions.
  • Transactions within messaging.
  • Lower volume, high reliability.

When Event Hubs

  • High throughput streaming.
  • Telemetry / IoT.
  • Replay / event sourcing.
  • Multiple consumer groups (different readers process same data).

Comparison

Feature Service Bus Event Hubs
Model Queue/Topic Log
Throughput ~100K/s Millions/s
Replay No Yes
Order Sessions Per partition
Consumer pattern Compete Consumer groups
DLQ Built-in Manual
Cost (per msg) Higher Lower

Authentication

Both: managed identity preferred over connection strings.

new ServiceBusClient("namespace.servicebus.windows.net", new DefaultAzureCredential())
new EventHubProducerClient("namespace.servicebus.windows.net", "hub", new DefaultAzureCredential())

Partner tools

  • MassTransit / Wolverine abstract Service Bus + RabbitMQ + Kafka. Sagas, outbox, retries.
  • Azure Functions triggers for both.

Schema Registry

Azure Schema Registry (built into Event Hubs Premium): Avro/JSON schemas tracked centrally; producers/consumers compatible.


Code: correct vs wrong

❌ Wrong: connection string in code

new ServiceBusClient("Endpoint=sb://...;SharedAccessKey=...");

✅ Correct: managed identity

new ServiceBusClient("ns.servicebus.windows.net", new DefaultAzureCredential());

❌ Wrong: ignoring DLQ

After 10 failures, message in DLQ. Without monitoring, accumulates.

✅ Correct: monitor DLQ + alert

Process or replay DLQ regularly.

❌ Wrong: ordering across customers

new ServiceBusMessage(json)   // no SessionId; random consumer

✅ Correct: session per customer

new ServiceBusMessage(json) { SessionId = customerId }

Design patterns for this topic

Pattern 1 — "Service Bus for business events; Event Hubs for streams"

  • Intent: match tool to workload.

Pattern 2 — "Sessions for per-entity ordering"

  • Intent: in-order processing.

Pattern 3 — "DLQ + monitor + replay"

  • Intent: poison message recovery.

Pattern 4 — "Capture for event archiving"

  • Intent: cheap event store.

Pattern 5 — "Managed identity over keys"

  • Intent: zero-secret.

Pros & cons / trade-offs

Service Pros Cons
Service Bus Rich features; sessions Cost per msg
Event Hubs High throughput; replay Manual DLQ; partition planning

When to use / when to avoid

  • Use Service Bus for business workflows.
  • Use Event Hubs for telemetry / streaming.
  • Avoid Service Bus for high-volume telemetry.
  • Avoid Event Hubs for command-style messaging.

Interview Q&A

Q1. Service Bus vs Event Hubs? Service Bus: queue/topic for business events. Event Hubs: log for streams.

Q2. Sessions? Group of messages same consumer, ordered. Per-customer state.

Q3. DLQ? Failed messages park here. Manual replay.

Q4. Duplicate detection? By MessageId in window. Broker dedupes.

Q5. Partitions in Event Hubs? Determines parallelism. Same key = same partition = ordered.

Q6. Consumer groups? Independent readers of same Event Hub. Different processors share data.

Q7. Capture? Auto-archive Event Hub data to Blob/Data Lake.

Q8. Kafka API? Event Hubs exposes Kafka-compatible endpoint. Use existing Kafka clients.

Q9. Premium SB benefits? Reserved capacity; geo-DR; private endpoint; 100MB messages.

Q10. Schema Registry? Built into EH Premium. Avro/JSON schemas centralized.

Q11. Service Bus transactions? Within SB only; not cross-resource. Use outbox for DB+SB.

Q12. Scheduled message? ServiceBusMessage with ScheduledEnqueueTime. Native scheduling.


Gotchas / common mistakes

  • ⚠️ Connection strings instead of managed identity.
  • ⚠️ DLQ unmonitored.
  • ⚠️ Wrong partition count in Event Hubs (can't change easily).
  • ⚠️ Sessions for unrelated messages — serialization bottleneck.
  • ⚠️ No idempotency in consumers — duplicates on redelivery.

Further reading