Drop a message on a Storage Queue, read it back, then do the same on a Service Bus queue and watch a poison message fall into the dead-letter.
The two shops that must not share a heartbeat
A checkout page has one job and a hard deadline: take the order and answer the customer in the time it takes to blink. Fulfilment has a different job and no deadline anyone is watching — capture the payment, reserve the stock, email the warehouse, tell the courier. For most of the bootcamp these two have talked the obvious way: the checkout calls fulfilment and waits for it to finish. That line is a wire welding two machines into one, and it means the fast machine can only be as fast, and as available, as the slow one. A warehouse API having a bad afternoon becomes a checkout page having a bad afternoon. A fulfilment outage becomes lost orders, because the only record of the sale was a function call that threw.
The fix is to stop making them talk directly and put a queue between them. The checkout writes one small message — order 8842 placed — to the queue and returns to the customer immediately; a separate worker reads that message later and does the slow work at its own pace. Nothing about the sale is lost if fulfilment is down, because the sale is sitting safely in the queue waiting; nothing about the checkout is slow if fulfilment is slow, because the checkout stopped waiting the moment it wrote the message. You have decoupled the two in three ways at once — in time (the work happens later), in availability (either side can be down while the other runs), and in scale (each grows to its own load, not the other's).
The payoff is loudest at the peak. Ten thousand orders arrive in the minute the Black Friday email lands; without a queue, ten thousand checkouts are all blocked on a fulfilment system built for a normal Tuesday, and the page falls over in front of the exact customers you spent money to attract. With a queue, ten thousand messages drop into it in that minute — writing to a queue is trivially fast — the checkout answers every customer instantly, and the worker drains the backlog over the following half hour while nobody waits. The queue did not make fulfilment faster. It made the checkout stop caring how fast fulfilment was.
A queue is a shock absorber, not a pipe.
Storage Queue — the simple bucket
Azure gives you two things called a queue, and the first is the one you already own. Back in Class Twelve, a storage account had four doors — blob, file, table, and queue. That fourth door is Azure Storage Queue, and it is deliberately almost featureless: a named bucket at a URL where you put a message and later get a message. A message is up to 64 KB of text, usually a small blob of JSON — an id and just enough to find the rest. Messages pile up in roughly the order they arrive but with no promise about it, live for up to seven days by default, and cost fractions of a cent for millions of them. There is no server to run; it is part of the storage account you already deployed.
- Storage Queue
- The queue service of an Azure storage account: a simple, cheap, high-volume bucket for messages up to 64 KB, with at-least-once delivery and no ordering, transactions, or publish-subscribe. The plainest possible way to decouple a producer from a consumer.
The one mechanic worth holding is how a message survives a crashed worker. When the worker gets a message it is not deleted — it is hidden, for a visibility timeout you choose, and the worker must explicitly delete it once the work is done. Finish in time and you delete it and it is gone. Crash halfway, and the timeout lapses and the message simply reappears for the next worker to try. That is the whole reason a queue is safe: nothing is thrown away on the strength of a promise to process it, only on proof that it was. It is also the root of the property §5 is built on — because "reappears if you don't confirm" means a message can be delivered more than once.
Reach for a Storage Queue when the job is honest and small: buffer a spike, hand work to a background worker, smooth a producer that is faster than its consumer — and when you do not need order, fan-out, or bookkeeping. When you find yourself wishing the bucket had those, you have not outgrown queues; you have outgrown this queue, and the next section is where you go.
Service Bus — the post office with rules
Azure Service Bus is the other queue, and it is a different class of object: a real enterprise message broker with its own namespace, not a corner of a storage account. Everything the Storage Queue refuses to promise, Service Bus makes its business. It comes in two shapes. A queue is point-to-point — many workers can compete for messages, but each message is handled once by one of them. A topic is publish-subscribe: the producer sends once, and every subscription attached to the topic gets its own independent copy. One order placed event can feed fulfilment, analytics, and fraud-screening at the same time, each reading at its own pace, none of them competing with or slowing the others.
- Service Bus
- Azure's managed enterprise message broker. Offers queues (compete for messages) and topics with subscriptions (publish once, deliver to many), plus ordering via sessions, built-in dead-lettering, duplicate detection, scheduled delivery, and transactions — the semantics financial and multi-team work depend on.
The semantics are the reason you pay for it. Sessions give strict first-in-first-out ordering within a key — all of one customer's events processed in the order they happened, which matters the instant a refund can outrun the charge it reverses. Dead-lettering is a built-in side queue where a message that cannot be processed goes to wait for a human, instead of looping forever. Duplicate detection discards a message you accidentally send twice; scheduled delivery holds a message until a future time; transactions let you complete one message and send another as a single all-or-nothing step. Messages are larger too — 256 KB on standard, up to 100 MB on premium — though the senior habit is still to send an id and keep the payload in storage, not to post the parcel through the letterbox. One mechanic on duplicate detection, because interviews probe it: it is not content-based magic — the broker remembers each message's MessageId for a window you configure and drops any newcomer carrying an id it has already seen, so set the id to something meaningful, like the order id, or the feature detects nothing.
A queue competes; a topic broadcasts.
The decision, priced
The interview question is never "what is Service Bus"; it is "queue or Service Bus, and why," and the senior answer is a decision rule with a cost attached — the same shape as Class Twenty-Seven's container verdict. Read the table as two columns of one question: do I need what only the broker gives?
| Question | Storage Queue | Service Bus |
|---|---|---|
| Order preserved? | No | Yes — FIFO within a session |
| One message, many readers? | No — one consumer group | Yes — topics + subscriptions |
| Dead-letter for poison messages? | Build it yourself | Built in |
| Duplicate detection / transactions? | No | Yes |
| Message size | Up to 64 KB | 256 KB · up to 100 MB (premium) |
| Delivery | At-least-once | At-least-once, peek-lock |
| Cost & setup | Pennies · already in your storage account | A namespace to run and pay for |
The rule falls straight out of the last two rows. Start with a Storage Queue whenever the job is plain decoupling — buffer, background work, smoothing a spike — because it is cheaper, simpler, and already deployed. Move to Service Bus the moment a named requirement appears that only it answers: ordering, publish-subscribe fan-out, dead-lettering, or transactions. The mistake that reads as junior is reaching for Service Bus first because it sounds enterprise; the move that reads as senior is refusing to pay for a broker until a requirement forces it, and then naming exactly which one did.1
At-least-once, and the two words that save you
Both services share one property that shapes every consumer you will ever write against them: delivery is at-least-once, never exactly-once. The reason is the §2 mechanic seen from the other side. A worker gets a message, does the work, and then — a network blip, a crash, a timeout — fails to confirm it before the lock lapses. The broker, having no proof the work was done, does the safe thing and delivers the message again. A message arriving twice is not a bug in Azure; it is the guarantee working correctly. Which means the bug is always yours if the second delivery does damage.
- Idempotent
- A worker is idempotent when processing the same message twice has the same effect as processing it once. The usual trick is a natural key — the order id — checked before acting: "have I already fulfilled 8842? then do nothing." At-least-once delivery is safe only against an idempotent consumer.
Say the two words in the interview and you have answered the follow-up before it is asked: make it idempotent. Charging a card twice because a message was redelivered is the canonical incident, and the canonical fix is not "try to get exactly-once from the broker" — you cannot — but "make the second time harmless" by keying on the order id and checking before you act. The same discipline covers retries, because a queue's whole model is retry-until-confirmed.
Service Bus makes the confirmation explicit, and names the choice. The default receive mode is peek-lock: the broker hands a worker the message and hides it, locked, until the worker calls complete (done — delete it) or abandon (release it for another try) — §2's visibility timeout given verbs, and the machinery under at-least-once. The alternative, receive-and-delete, removes the message the instant it is read: one round-trip, fast, and at-most-once — a worker that crashes mid-job takes the message down with it, unrecoverably. Receive-and-delete has honest uses — telemetry where one lost reading costs nothing — but choosing it for orders trades "a message might arrive twice" for "an order might silently vanish," which is the worse Saturday. The default is the default for a reason.
The other message you must have an answer for is the one that can never succeed — a malformed order, a reference to a product that was deleted, a bug that throws on this exact payload every time. Left alone it becomes a poison message: it is delivered, it fails, it reappears, it fails, forever, and on a single-lane queue it wedges everything behind it. The answer is a delivery-count limit and a dead-letter destination — after, say, ten attempts the message is moved aside to a side queue where a human reads it on Monday, and the line keeps moving. Service Bus gives you this for free; on a Storage Queue you build it yourself from the dequeue count. "What happens to a message that fails a hundred times?" has exactly one good answer, and it is not "it retries a hundred times."2
At-least-once means: make the second time harmless.
The checkout stops waiting for the warehouse
The symptom arrives as a support ticket during a modest promotion: checkouts are timing out, and the storefront is fine — it is the warehouse API behind it, slow under load, and the checkout is welded to it by a direct call that waits. The fix is a queue. A Storage Queue named order-events in the storage account Campux already runs; the checkout now writes the order id and returns in milliseconds, and a background worker — the Function App you build next class — drains the queue and does the slow fulfilment work behind the scenes. The next promotion's spike lands in the queue instead of on the page; the worker catches up in twenty minutes, the visibility timeout quietly re-serves the handful of messages a worker restart interrupted, and no customer waits on the warehouse again.
Two requirements then outgrow the bucket, exactly as §4 predicts. Finance finds a refund that processed before the charge it reversed, because a Storage Queue never promised order — they need a customer's events handled in sequence. And the new analytics team wants its own copy of every order event, without racing fulfilment for it. Both are Service Bus's job: the order-events queue becomes a topic named orders with two subscriptions, fulfilment and analytics, and sessions keyed on customer id so one shopper's events stay in order. The worker is already idempotent — it keyed on the order id from the first day, per §5 — so the switch changes the plumbing and not the code that drains it. When a partner's malformed test order arrives in month two, it fails, retries to its limit, and drops into the dead-letter where an engineer reads it on Monday; the line behind it never stops. The checkout page, three requirements and two services later, still does the one thing it always did: take the order, and answer the customer in a blink.
The official pages, and a CAMPUX overview
Storage queues and Service Bus queues — compared and contrasted
learn.microsoft.com/azure/service-bus-messaging/service-bus-azure-and-service-bus-queues-compared-contrasted
Service Bus messaging — overview (queues, topics, dead-letter, sessions)
learn.microsoft.com/azure/service-bus-messaging/service-bus-messaging-overview
A message put on a Storage Queue and read back, then a Service Bus queue with a delivery limit, a poison message failing its three attempts, and the dead-letter sub-queue filling up in Service Bus Explorer — will live here. Video to be added.
A Storage Queue, a Service Bus queue, and a message that won't die
Send and receive on the simple bucket, then on the broker — and watch a poison message land in the dead-letter instead of looping forever.
Create a storage account and a queue, then put a message and read it back. One grant first: --auth-mode login authenticates with your Entra identity, and queue data is guarded by a data-plane role that even a subscription Owner does not implicitly hold — without it, every queue command below fails with AuthorizationPermissionMismatch. That refusal is Class 8's control-plane/data-plane line, met in the wild:
az group create --name rg-queue-lab --location eastus az storage account create -n stqlab<initials> -g rg-queue-lab --sku Standard_LRS az role assignment create \ --assignee $(az ad signed-in-user show --query id -o tsv) \ --role "Storage Queue Data Contributor" \ --scope $(az storage account show -n stqlab<initials> \ -g rg-queue-lab --query id -o tsv) # wait ~60s for the role to propagate, then: az storage queue create -n order-events --account-name stqlab<initials> --auth-mode login az storage message put -q order-events --content "order 8842 placed" \ --account-name stqlab<initials> --auth-mode login az storage message peek -q order-events --account-name stqlab<initials> --auth-mode loginWhat to notice: the queue is just part of the storage account you already know how to make. peek reads without removing — the message is still there for a real worker to get, process, and delete. That put-hide-delete cycle is the whole safety mechanism from §2.Now create a Service Bus namespace and a queue with a delivery limit, so failures are capped rather than infinite:
az servicebus namespace create -n sb-campux-<initials> -g rg-queue-lab --sku Standard az servicebus queue create -n orders -g rg-queue-lab \ --namespace-name sb-campux-<initials> --max-delivery-count 3
What to notice: a namespace is its own resource, not a corner of a storage account — that is the line between the bucket and the broker. --max-delivery-count 3 is the poison-message guard: after three failed attempts the message is dead-lettered instead of retried forever.In the portal, open the namespace → Service Bus Explorer on the orders queue. Send a message, then Peek it. Then switch the sub-queue selector to Dead-letter to see the (empty for now) side queue where poison messages wait.
What to notice: the Explorer is the broker's own inbox. A real worker would receive (peek-lock), process, and complete the message; abandon it three times and it moves to the dead-letter sub-queue you just opened — visible, countable, and waiting for a human, not lost and not looping.Tear it all down in one line so nothing bills overnight:
az group delete --name rg-queue-lab --yes --no-wait
The lesson: two queues, two philosophies. The Storage Queue asked nothing of you and promised little; the Service Bus queue asked for a namespace and a delivery limit and gave you dead-lettering in return. You chose complexity only where a requirement — capped failure — demanded it.
Zoom out: a queue moves the bottleneck, it does not remove it
You put a queue between two systems and the checkout stopped falling over. Now reason about what the queue did to the whole system, not just to one order — because a buffer changes where the pressure goes, not whether there is pressure. Take a whiteboard and work these five prompts before the exam.
The producer is faster than the consumer, so the queue grows. Nothing pushes back — a queue's whole point is to accept the spike — so the backlog climbs, and with it the age of the oldest message and the delay a customer's order waits. What balancing control (autoscaling the consumers on queue depth, alerting on backlog age) keeps "buffered" from quietly becoming "hours behind"?
The queue decoupled the producer and consumer from each other — but coupled both to the broker. Map what fails now: if the consumer is down, orders pile up safely; if the broker is down, the producer cannot even enqueue, and you are back to a synchronous failure with extra steps. The decoupling is real; it is not free of dependency, it relocated it.
The throughput ceiling of the whole pipeline is the consumer's processing rate, not the queue's. Add producers and the backlog grows; the wall is how fast the worker drains. When orders double, the lever is more or faster consumers — the queue was never the limit, it was the waiting room.
At-least-once delivery forced you to make the worker idempotent — which means a store of "orders already processed" to check against. That store is now a new dependency, a new cost, and a new thing that can be slow or wrong. Buying safe redelivery cost you a lookup on every message; who owns that store, and what happens when it is down?
One queue is one lane. A single poison message on a strict-order session blocks everything behind it; one slow message type starves the fast ones. At ten times the volume, do you partition by message type, split into per-priority queues, or shard by session key — and which of those is a config change versus a re-architecture?
The engineer who ships is asked "does it work?" The engineer who gets promoted is asked "and where did the pressure go?" — and has already drawn the backlog climbing.
Cutting the wire between two systems
A checkout calls a fulfilment service directly and waits, and every time fulfilment has a slow afternoon the storefront has one too. You put a queue between them: the checkout writes a message and returns, a worker drains it at its own pace, and the two systems stop sharing a heartbeat. The next outage downstream is a growing backlog instead of a homepage on fire — and you are the one who cut the wire.
Examination
Four drills, then two situations. The situations have no marking scheme — write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored; this is between you and the page.
C — the requirement names two things only Service Bus gives, so the service picks itself. "In order, per customer" is sessions (FIFO within a key); "its own independent copy" is publish-subscribe, which is a topic with a subscription each. A fails on both counts — a Storage Queue promises no order and has no fan-out. B fakes fan-out by writing twice, which means the producer now knows about every consumer and must change whenever one is added — the coupling a topic exists to remove. D is the subtlest wrong answer: a single Service Bus queue is point-to-point, so fulfilment and analytics would compete for messages and each event would be seen by only one of them — analytics would get half the orders and fulfilment the other half. The tell that forces a topic over a queue is the phrase "its own copy."
B — at-least-once is the guarantee, not the bug, so you fix the consumer, not the broker. A message redelivered because a worker crashed mid-process is delivery working correctly; the defect is a worker that does damage the second time. Keying on the order id — "have I already charged 8842? then don't" — makes the second delivery harmless, which is the only durable answer. A asks for something neither service offers: exactly-once across a distributed boundary is not on the menu, and a candidate who claims it is has revealed they do not know the model. C makes double-delivery rarer, not impossible — a longer or shorter timeout still cannot prevent a crash after the work but before the confirm — so it is a smaller window around the same bug. D is the catastrophe: delete-before-process means a crash mid-work loses the order entirely, trading a double-charge for a silent disappearance, which is worse.
Sessions, topics, and built-in dead-lettering — the three the broker charges you for. Each is a semantic a Storage Queue simply does not have: order within a key, fan-out to many readers, and a managed side queue for poison messages. The two rejects are the discriminators. At-least-once delivery is not a Service Bus advantage — both services deliver at-least-once, so it cannot be a reason to choose one over the other; the option is true but answers the wrong question. And "part of a storage account, fractions of a cent" describes the Storage Queue, not Service Bus — it is the bucket's headline feature offered as a distractor. Reading comprehension is half of this drill: the question asked what Service Bus adds, and two options quietly describe its cheaper cousin.
# design: order-events consumer
1. Consumer receives with peek-lock, processes, then
completes the message on success.
2. Consumer is idempotent — keyed on the order id.
3. On any processing error, abandon the message so it
is retried; retry forever until it eventually works.
4. Poison messages are expected to be rare and are
reviewed by an engineer from the dead-letter queue.
Line three — and it contradicts line four, which is how you catch it in review. "Retry forever until it eventually works" assumes every failure is transient. A poison message — malformed, referencing a deleted product, tripping a bug on this exact payload — will never work, so "retry forever" means retry forever: the message is redelivered, fails, redelivered, fails, and on a single ordered lane it blocks every message behind it while it does. The fix is a delivery-count limit (the lab's --max-delivery-count) that dead-letters the message after N attempts — which is exactly what line four assumes already exists. A plan that both retries forever and reviews the dead-letter is describing two incompatible worlds; the dead-letter only ever fills if something stops the retrying. Lines one and two are correct and load-bearing: peek-lock-then-complete is the safe receive cycle, and idempotency (B is the distractor) is required precisely because the lock can lapse and redeliver. The reviewer's reflex: when two lines of a plan cannot both be true, one of them is the bug.
Agree on the emergency, then separate the two ideas — one is triage, one is the fix. Mid-incident the lead is right that re-architecting now is reckless, so concede the timeframe first. But their two suggestions are not equivalent. A retry with backoff on a synchronous call to an already overloaded service is pouring water on a grease fire: retries are more load on the thing that is falling over, and the checkout is still welded to fulfilment's health. Caching is better as triage — it can spare the backend some reads — but fulfilment is a write path (capture payment, reserve stock), and you cannot cache a write. So for tonight: shed load, add a circuit breaker so the checkout fails fast and queues nothing rather than hammering a dead service, and get fulfilment breathing.
Name the disease so the fix isn't forgotten once the fire is out. The timeout is a symptom; the disease is that the checkout's availability is chained to fulfilment's, because they talk synchronously. No amount of retry or cache changes that — they tune the coupling, they do not cut it. The only thing that cuts it is a queue: the checkout writes "order placed" and returns, fulfilment drains at its own pace, and the next time fulfilment has a bad afternoon it is a growing backlog, not a homepage on fire. Say plainly that tonight's mitigations buy time and next sprint's queue buys the cure, and that skipping the second means booking this same incident again.
Close on the test that tells triage from cure. Ask of each idea: if fulfilment goes fully down for an hour, what happens to a customer trying to check out? With retry or cache, the checkout still fails — it is just failing more politely. With a queue, the customer checks out fine and their order waits safely to be fulfilled when the service returns. That question is the one to put on the incident review, because it sorts the patches from the fix in a single sentence.
Grant the instinct, because it is mostly a good one. Fewer technologies is fewer things to learn, monitor, secure, and page someone about at 3am — the operational argument for standardising is real, and a team fluent in one broker will run it better than a team spread across two. If the estate already needs Service Bus for its ordering and pub-sub work, defaulting new async work there too is a defensible call, not a mistake. Start by agreeing that "one technology" is a genuine benefit, so the disagreement is about price, not principle.
Then price what the standard costs on the simple end. A Storage Queue is a corner of a storage account you already run — no namespace, near-zero cost, nothing extra to operate. Standardising on Service Bus means every trivial "buffer this background job" now provisions and pays for a broker's worth of machinery it will never use: no sessions, no topics, no dead-letter needed, just a bucket that now costs a namespace. Multiply that across an estate and the "simpler to operate" saving is partly eaten by paying broker prices for bucket problems. The honest framing is not "one versus two technologies" but "a small standardisation tax on every simple case versus a small fluency tax on running two."
Close on the variable that decides, so it is a rule and not a preference. The tie-breaker is how often the simple case actually occurs and how cost-sensitive the estate is. A large estate with many trivial background jobs and a tight Class 32 budget should keep the Storage Queue as the default and reserve Service Bus for the requirements that name it; a smaller estate that already lives in Service Bus for its core flows can reasonably standardise and eat the small tax for the operational simplicity. Offer the rule you would actually write: Service Bus by default only where the estate is already fluent in it and simple cases are rare; otherwise, cheapest tool that meets the requirement, named per case. That keeps the §4 discipline intact while honouring the real value of not running two of everything.
Five things worth carrying out of this class
- A queue decouples a producer from a consumer in time, availability, and scale — the fast machine stops being chained to the slow one. A queue is a shock absorber, not a pipe.
- Storage Queue is the simple bucket: cheap, part of a storage account, up to 64 KB, no ordering or fan-out. Reach for it first, for plain decoupling.
- Service Bus is the broker: queues that compete and topics that broadcast, plus sessions (ordering), dead-lettering, duplicate detection, and transactions. Move to it when a named requirement demands one of those — not because it sounds enterprise.
- Both deliver at-least-once, so the consumer must be idempotent — key on a natural id and make the second delivery harmless. Exactly-once is not on the menu.
- A poison message that can never succeed must be capped and dead-lettered after N attempts, not retried forever — otherwise it wedges the line behind it.
- Storage Queues and Service Bus are two of a larger messaging family, and interviewers sometimes probe the edges: Event Hubs is for high-throughput event streams (telemetry, clickstream — millions of events, read by position, not deleted per-message), and Event Grid is for reactive event routing (a blob was created, fire a handler). Queues are for work that must be done once and confirmed; the others are for events observed by many. Out of scope here, but know the four names apart. ↩
- "At-least-once" and "exactly-once" are the honest labels; you will hear vendors and blog posts claim exactly-once, and the claim is always doing quiet work — usually "at-least-once delivery plus idempotent processing," which is exactly the §5 pattern with a shinier name. Treat any bare "exactly-once" promise with suspicion and ask where the deduplication actually happens; the answer is always "in a consumer that checks before it acts." ↩
- This class teaches Azure's two queues because they are what AZ-204 and the estate expect, but the pattern outranks the products: a producer, a durable buffer, a competing or broadcasting consumer, at-least-once delivery, idempotency, and a dead-letter for the poison. Meet RabbitMQ, Amazon SQS, Kafka, or Google Pub/Sub and you will find the same nouns wearing different names — learn the shape here once, and the products become configuration. ↩