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
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).
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
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
✅ Correct: managed identity
❌ 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
✅ Correct: session per customer
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.