Message queues
A point-to-point queue decouples producer from consumer in time, so a slow or dead downstream can't take the caller with it. The cost is delivery semantics: at-least-once and at-most-once are the only honest options, and 'exactly-once' is really effectively-once via idempotency.
A checkout service called the email service synchronously to send a receipt. One afternoon the email provider got slow — 8 seconds per call instead of 80 ms. Within minutes checkout itself was timing out: every payment thread was blocked waiting on email, the pool drained, and a non-critical dependency had taken down the part of the system that makes money. The fix wasn’t a faster email provider. It was to stop coupling the two in time at all: drop a “send receipt” message into a queue, return to the customer immediately, and let a separate worker drain the queue at whatever pace email allows. The receipt is now late, not lost — and checkout never notices email is sick.
What a point-to-point queue actually buys you
A message queue is a durable buffer between two parties. A producer appends a message; the queue persists it; a consumer pulls it later, processes it, and acknowledges. “Point-to-point” means each message is delivered to one consumer in the group and then removed — contrast that with the fan-out of pub/sub in the next lesson, where every subscriber gets a copy. The defining property is temporal decoupling: the producer and consumer no longer have to be alive, fast, and reachable at the same instant.
That single property buys three things at once. Load levelling — a spike of 10,000 enqueues doesn’t have to be processed at 10,000/sec; the queue absorbs the burst and the consumer drains it at its sustainable rate. Failure isolation — if the consumer crashes, messages wait in the queue instead of erroring back to the producer; the work resumes when the consumer recovers. Independent scaling — you scale producers and consumers separately, adding consumer instances to drain faster without touching the producer.
The senior framing is that a queue converts a synchronous availability dependency into an asynchronous latency dependency. Synchronously, your availability is the product of every downstream’s availability — three 99.9% services in a call chain give you 99.7%. Put a durable queue between them and the producer’s success no longer depends on the consumer being up; it depends only on the queue being up. You’ve traded “the whole request fails when email is down” for “the receipt arrives a few minutes late.”
The honest delivery semantics: at-least-once vs at-most-once
Here is the part juniors skip and seniors design around. A queue and its consumer talk over an unreliable network, and the consumer can crash at any point. That forces a choice about when you acknowledge a message, and the choice fixes your delivery guarantee:
at-most-once ack BEFORE processing → if the worker crashes mid-process,
the message is gone. No duplicates, possible loss.
at-least-once ack AFTER processing → if the worker crashes after the work
but before the ack, the message is redelivered. No loss,
possible duplicates.There is no third honest option over an unreliable network. You either risk losing a message (ack early) or risk processing it twice (ack late). Nearly every durable queue — SQS, RabbitMQ, classic JMS — defaults to at-least-once, because for most systems a duplicate is recoverable but a lost order is not. The consequence you must internalize: your consumer will, eventually, see the same message twice. Design as if it’s guaranteed, because at scale it is.
▸Why this works
Why can’t the broker just give you exactly-once and end the debate? Because “process the message” and “acknowledge the message” are two separate actions, and no protocol can make them a single atomic step across a network with crashes. Whatever order you put them in, a crash can land in the gap between them: ack-then-process can lose the work, process-then-ack can repeat it. The broker can make its own storage exactly-once (write the message once), but it cannot reach into your handler — which might charge a card or send an email — and make that side effect atomic with the ack. So the only place exactly-once can actually be enforced is inside your consumer, by making reprocessing harmless.
”Exactly-once” is effectively-once: idempotency + dedup
When a vendor sells “exactly-once,” they mean effectively-once: the message may be delivered more than once, but its effect happens once. You get there with two tools working together. Idempotency — design the operation so applying it twice equals applying it once: set balance = 100 is idempotent; add 100 to balance is not. Deduplication — give each message a stable business key (an order ID, a payment intent ID) and keep a record of keys you’ve already processed; on redelivery, you see the key, skip the work, and just re-ack.
producer: enqueue { idempotency_key: "order-8842", action: "charge $50" }
consumer on each delivery:
if seen("order-8842"): ack and stop ← dedup catches redelivery
else: charge, mark seen("order-8842"), ackThe dedup store (a row with a unique constraint, a Redis key with a TTL) is what turns at-least-once delivery into effectively-once processing. This is why the right mental model is never “I’ll find a queue that won’t duplicate.” It’s “I’ll accept duplicates at the door and make them harmless inside.” Effectively-once is a property of your code, not a checkbox on the broker.
The mechanics that make at-least-once work: visibility timeout, DLQ, ordering
Three mechanisms turn the theory into a running system:
- Visibility timeout. When a consumer pulls a message, the queue doesn’t delete it — it hides it for a configured window. If the consumer finishes and deletes it within the window, done. If the consumer crashes (never deletes), the timeout expires and the message reappears for another worker. This is how at-least-once redelivery actually happens. Set it too short and a slow-but-alive worker’s message reappears and gets processed twice; set it too long and a genuinely crashed message takes ages to retry. Long jobs must heartbeat — extend the timeout while still working — or the queue will hand the same message to a second worker and you get parallel duplicate processing.
- Dead-letter queue (DLQ). A message that fails repeatedly (a “poison” message — malformed input, a bug) would otherwise be redelivered forever, burning capacity and blocking the queue. After N failed attempts the broker moves it to a separate dead-letter queue for human inspection. Alarm on DLQ depth: a non-empty DLQ means a bug, not a transient blip.
- Ordering. A plain queue with many parallel consumers makes no global ordering promise — message B can finish before message A. If you need order, you need ordering per key (SQS FIFO message groups, a single partition), which costs throughput because messages in one group can’t be processed in parallel. Most systems don’t need global order; they need order within a single entity (one user’s events) and design the key accordingly.
▸Common mistake
The classic visibility-timeout bug: a worker pulls a message, the work takes longer than the timeout (a slow dependency, a big batch), and while it’s still running the message reappears and a second worker starts the same job. Now you have two workers charging the same card in parallel — and if their work also takes too long, a third joins, and the service “fork-bombs itself” under load exactly when latency is already high. The fixes are to size the timeout above your real p99 processing time, to heartbeat long jobs (extend the timeout as you go), and — always — to make the handler idempotent so the duplicate is harmless even when the timeout is wrong.
Your queue is configured at-least-once and your consumer charges a credit card on each message. What is the minimum correct design so a redelivery doesn't double-charge?
A worker pulls a message, but the job legitimately takes 90 seconds while the visibility timeout is 30 seconds. What happens, and what's the right fix?
Because a durable queue defaults to at-least-once delivery, your consumer must be _______ — applying the same message twice has the same effect as applying it once — so that the redeliveries the visibility timeout inevitably causes are harmless.
This unit stays at system-composition altitude — where a queue sits in an architecture and what guarantee it changes. The dedicated queues track goes a level deeper into the brokers themselves: how SQS, RabbitMQ, and Kafka implement delivery, the exact ack protocols, and broker tuning. Reach for it when you’re configuring a specific broker; stay here when you’re deciding whether a queue belongs in the design at all.
- 01What does a point-to-point queue decouple, and what does it cost?
- 02Contrast at-least-once and at-most-once, and explain why exactly-once is really effectively-once.
- 03What do visibility timeout, DLQ, and ordering each handle?
A point-to-point message queue is a durable buffer that decouples a producer from a consumer in time: the producer enqueues and moves on, a consumer drains later, and each message is delivered to one consumer and removed. That single property converts a synchronous availability dependency (the request fails if the downstream is down) into an asynchronous latency dependency (the work just arrives later), buying load levelling, failure isolation, and independent scaling. The price is delivery semantics: because “process” and “acknowledge” can’t be atomic over an unreliable network, you choose at-most-once (ack first → no duplicates, possible loss) or at-least-once (ack last → no loss, possible duplicates), and almost everyone picks at-least-once. So-called exactly-once is really effectively-once — you accept duplicates at the door and make them harmless with idempotency + a dedup key, enforced in your own code, never by a broker checkbox. The running machinery is the visibility timeout (the engine of redelivery — size it above p99, heartbeat long jobs), the dead-letter queue (sideline poison messages, alarm on its depth), and per-key ordering when you truly need it. Decide here whether a queue belongs in the design; drop into the dedicated queues track when you’re tuning a specific broker. Now when you see a slow downstream cascading into a caller timeout, ask first: is there a durable queue between them — and if so, does the consumer have an idempotency key?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.