Service Bus or Event Hubs

Every Service Bus queue comes with a dead-letter queue you cannot delete, and an Event Hubs partition has nowhere to put a record that will not process. Both are a checkbox away on Azure, so this is about which one you buy: what the quota tables commit you to, and why the throughput comparison is the one axis where they look alike. The tell is whether a single message can fail on its own.

Instruction or Observation

Service Bus is a broker. It holds messages, hands one to a consumer under a lock, waits to be told what happened, and takes the message back if it is not told. Event Hubs is a partitioned log. It appends records, keeps them for a fixed window, and lets any number of readers move a cursor through them at whatever pace they like. Almost everything else about the two products is a consequence of that sentence.

The decision is usually framed as a throughput question, and I think that framing is responsible for most of the bad outcomes. It is not that throughput is irrelevant; it is that both products will comfortably carry more traffic than most systems generate, so the number rarely discriminates, and choosing on it means choosing on the one axis where the two happen to look similar. Then the difference shows up months later, in an incident, when somebody needs one particular message to not be processed.

The rule I use instead is about what the message is. An instruction is addressed to somebody: charge this card, provision this account, send this letter. Exactly one handler should act on it, that handler owes you an answer, and if the answer is no the message must go somewhere a human can look. An observation is addressed to nobody: this sensor read 41 degrees, this page was viewed, this order changed state. Several unrelated consumers may care, at different speeds, and one may want last Tuesday again.

The generic semantics behind that - what a queue is, what a topic is, what a stream is, and why "exactly once" is mostly a story people tell about their own retries - I have written about separately, and the operational cost of running a broker instead of buying one has its own piece. This is narrower. Given that you are on Azure and both of these are a checkbox away, which one do you buy, and what does it cost you when you buy the wrong one.

One Message That Fails

Service Bus settles each message individually. A consumer receives under a peek-lock and then completes, abandons, defers, or dead-letters. Those are four different statements about one message and they have no effect on any other message. Nothing about that flow requires the consumer to process anything in order, or to have processed the previous message at all, which is why competing consumers on one queue is the natural shape and adding a fifth worker is a deployment rather than a design change.

The dead-letter queue is not a pattern you implement. Every queue and every subscription already has one. Per Microsoft's documentation it cannot be deleted or managed independently, and the broker fills it on its own: delivery count exceeded, time to live expired, header too large, a null session identifier on a session-enabled entity, too many auto-forward hops. The default delivery limit is 10, which means a message that keeps failing removes itself from your critical path after ten attempts without anybody writing code.

A log has no per-message anything. An Event Hubs consumer holds an offset in a partition, and the only thing it can record is how far it has read. There is no "this one failed" that the service understands, because the record is not addressed to it and other consumers are reading the same record for their own reasons. Skipping a bad record means advancing your offset past it, which is a decision your code makes and your code has to remember, forever, in storage you provisioned.

This is why the poison-message story has to be built. The working pattern is to catch the failure, write the record and the reason somewhere durable of your own, advance the offset, and give somebody a way to inspect and replay that store later. That is a queue with a dead-letter queue, hand-built, sitting beside a log. It is perfectly reasonable engineering and I have seen it done well. It is also several weeks of work, plus a runbook, to reacquire something the other product hands you switched on.

Order Against Parallelism

Service Bus does ordering with sessions. Set a session identifier on a message and the broker guarantees that messages sharing that identifier are delivered in order, to a single receiver holding a lock on that session, and that no other receiver touches it while the lock is held. Your parallelism is therefore the number of distinct sessions in flight rather than the number of consumers you deployed, which is a genuinely useful property: order per customer, per account, per device, and unlimited concurrency across them.

The cost is that sessions are sticky and their behavior at the edges is strict. A session-enabled entity refuses messages without a session identifier and dead-letters them with that as the reason, which is a good failure but catches people the first time. And one slow session is one blocked receiver, so a customer generating a pathological volume can occupy a consumer while everything else waits, which is a shape of incident that looks like a general slowdown and is not.

Event Hubs does ordering with partitions. A partition key hashes to a partition, records within a partition are strictly ordered, and there is no order at all between partitions. Your parallelism is the partition count, and a consumer group can usefully have at most one active reader per partition - the documented limit is five non-epoch receivers per consumer group, which is a ceiling on readers rather than a way to spread load. So the partition count is your concurrency ceiling, chosen in advance.

And on the Standard tier that choice is permanent. An event hub there tops out at 32 partitions and the count is fixed when you create it; dynamic partition scale-out is a Premium and Dedicated capability, where the ceiling is 100 partitions per hub. Which means the most consequential capacity decision in the product is made on the afternoon somebody clicks Create, by whoever clicked it, usually before there is any traffic to reason about.

What Happens Afterward

A settled message is gone, and that is the feature. Complete a Service Bus message and the broker has no further record of it. The queue depth is your backlog and an empty queue means the work is done, which makes the single most useful operational metric in the product a number anybody can read without interpretation. It also means that if you processed a message wrongly, the message is not there to process again, and your recovery is whatever your upstream can re-send.

A hub keeps everything for its window regardless. Retention is time-based and indifferent to who read what: 1 day on Basic, 7 days on Standard, and up to 90 days on Premium and Dedicated. Nothing a consumer does shortens it. That is exactly what makes reprocessing possible - deploy the fix, rewind the offset, run the last four days through the corrected code - and it is the single strongest argument for putting an observation in a log rather than a queue.

The window is also a standing bill and a compliance surface. Retention storage is metered against your capacity: 84 GB per throughput unit on the Standard tier, 1 TB per processing unit on Premium, 10 TB per capacity unit on Dedicated. So a longer window either fits in what you already bought or it makes you buy more units, and the units are what you were sizing for throughput. Ninety days of retention is also ninety days of anything a customer asked you to delete.

Replay is not free of consequences downstream. Rewinding an offset re-delivers records to a consumer group, and everything that consumer does on the way past happens again. If it writes to a database, it must be idempotent. If it sends email, somebody gets four copies. The ability to replay is real and it moves the burden rather than removing it: from the messaging layer, where the broker would have quarantined one bad message, into every consumer, permanently.

The Shape of the Bill

Event Hubs meters capacity. On Standard you buy throughput units, and one of them is defined as 1 MB per second or 1,000 events per second inbound and 2 MB per second or 4,096 events per second outbound, with a ceiling of forty of them. Note that both halves of the definition apply: a firehose of very small events hits the events-per-second half long before the megabytes, which is the single most common capacity surprise in the product and it looks like throttling for no reason.

Service Bus Premium meters messaging units, which are a slab of reserved capacity rather than a rate, and the published limits change character accordingly: Basic and Standard are capped at 1,000 operations per second while Premium has no fixed operations ceiling and gives you 1 TB of namespace per messaging unit. Premium is also where the interesting durability lives, since geo-replication there copies message data and message state to a secondary region, where the older geo-disaster-recovery pairing copied entity metadata only. Those are very different promises with confusingly similar names.

Message size is the limit that most often forces the tier. Basic and Standard Service Bus cap a message at 256 KB, which sounds generous until somebody attaches a document; Premium defaults to 1 MB per entity and can be raised to 100 MB per queue or topic over AMQP, though a batch is still capped at 1 MB on every protocol. Event Hubs allows 1 MB per publication on Standard and Premium alike. The honest answer for anything larger is a pointer to blob storage, in both products.

Event Hubs also speaks the Kafka protocol on Standard and above, and this is worth being precise about because it gets used as a reason to choose. It is a compatibility endpoint: existing Kafka clients, connectors and tooling work, and the exit is not a rewrite. It does not make Event Hubs into Kafka, it does not bring the parts of that ecosystem that run as separate services, and it is a portability argument rather than a capability one.

The Mistake With a Shape

It always starts as a cost decision that is correct. A team has work items to distribute, prices both products at their expected volume, and finds that ingestion into a hub is cheaper than a broker namespace sized for the same rate. That comparison is not wrong. It is comparing the two products on the axis where they are comparable and ignoring the one where they are not, and nothing in the first six months contradicts it, because for six months every message succeeds.

The bill arrives as an incident, not as an invoice. One record cannot be processed. There is no retry the service performs for you, no dead-letter queue to put it in, and no way to skip it without moving an offset past records behind it that were fine. The realistic choices at three in the morning are to stop the consumer, or to advance past a range and lose whatever else was in it. Both are decisions somebody has to make live, with no tooling, having never rehearsed it.

There is a tell you can check today. Ask whether a single message can fail on its own without meaning the pipeline is broken. If the answer is yes, and somebody would want to look at that one message, retry it, or hand it back to a human, you are describing a broker, and you should buy the broker. If a failed record means your consumer has a bug affecting all of them, and the fix is to deploy and reprocess, you are describing a log, and the log is right.

When It Is Neither

Azure Storage queues sit underneath both and get overlooked because they are unfashionable. A queue in a storage account, messages up to 64 KB, at-least-once delivery, a visibility timeout, a dequeue count you can read, and effectively no capacity planning. There are no topics, no sessions, no transactions, and no dead-letter queue of its own - the poison queue people remember is something the Functions host builds on top after a configurable number of attempts. For a background job that one worker picks up, this is the whole requirement.

At the other end, if what you want is for something to happen when a thing occurs - a blob landed, a resource changed, a service raised an event - that is Event Grid, which pushes to your handler, filters on the event rather than making you read everything, retries with backoff, and drops what it cannot deliver into a dead-letter destination you nominate. Reaching for a hub or a queue there means building a poller and a dispatcher to reproduce what the routing service already does.

I would put the whole decision this way. Storage queues when one worker does one job and nobody needs to see the machinery. Event Grid when the requirement is a reaction. Service Bus when a message is an instruction that has to be answered for. Event Hubs when a message is a fact several people will read at their own speed and somebody will want to read again. Four products, four questions, and the questions are not about scale.

I should narrow one thing, because the piece has been tidier than the world is. Plenty of real systems need both, and the pairing is not a compromise: the hub takes the raw arrivals, one consumer group reads them and decides which require somebody to act, and puts those on a queue as instructions with their own retries and dead letter. That is right more often than either product alone, because it stops asking one thing to be both a record and an order.

The other thing I would carry away is smaller and duller. Every limit I have quoted came off Microsoft's published tables while writing this, because I have been wrong about them before and so has everyone who repeats them from a conference talk. Partition counts, retention ceilings, message sizes and tier boundaries have all moved, in both directions, more than once. The decision rule in this piece will outlive the numbers in it, which is the only reason it is worth writing down.