Hotshard
Open the simulatorSimulator
Distributed messaging

Publish / Subscribe

One message, published once, delivered to many independent subscribers — each with its own progress, its own retries, and its own way to fail.

A customer places an order. Billing has to charge the card, analytics has to count the sale, and the email service has to send a receipt. Three teams, three jobs, all triggered by the same event. Publish/subscribe is the messaging pattern that lets the order service announce the event once and walk away, while each of those consumers reacts on its own schedule and recovers from its own failures. This page builds the pattern up from that problem: how one publish fans out to many subscriptions, how each subscription tracks its own copy of the message, and what "at-least-once delivery" really costs you.

Open the simulator →13 min

The problem: one event, many independent reactions#

The naive version is a direct call. The order service calls billing, then calls analytics, then calls email. This breaks in three ways at once. It is slow, because the order service waits for all three before it can respond. It is fragile, because if the email service is down the whole order fails. And it is rigid, because adding a fourth consumer means editing and redeploying the order service.

What we actually want is for the order service to state a fact once, "order 4271 was placed," and be done. Whoever cares about that fact can react to it, in their own time, without the order service knowing they exist. If one reactor is slow or broken, the others should be unaffected, and the broken one should get another chance later rather than losing the event.

Publish/subscribe is the decoupling that gives you exactly this. A producer publishes messages to a named topic. Interested consumers attach a subscription to that topic. A broker in the middle copies every published message to every subscription and then tracks, per subscription, whether that copy has been handled yet. The producer and the consumers never reference each other. They only reference the topic.

The mental model: a topic is a loudspeaker, a subscription is a mailbox#

Picture the topic as a loudspeaker in a room. When the producer speaks, everyone with a mailbox on the wall gets a written copy dropped into their own box. The speaker does not wait for anyone to read it, and does not even look up to see who is in the room.

Each mailbox is a subscription. The copy sits in the mailbox until its owner picks it up, works through it, and signs a receipt. If the owner drops the copy or never signs, the broker eventually notices and drops a fresh copy in the same box. Crucially, one owner signing their receipt does nothing to anyone else's mailbox. Every mailbox has its own copy and its own pace. That independence is the whole point of the pattern, and it is what a plain shared queue cannot give you.

The one line to rememberPublish is fan-out plus per-subscriber bookkeeping: the message is copied once per subscription, and each subscription privately tracks whether its own copy is done.

How it works: fan-out and the delivery lifecycle#

There are two mechanisms stacked on top of each other. The first is fan-out: turning one publish into one copy per subscription. The second is the delivery lifecycle: how the broker walks a single copy from arrival to done, with retries in between.

When the producer publishes a message, the broker assigns it an id, records it on the topic once, and then places an independent copy into every subscription's pending set. A subscription with nothing attached to it still gets its copy; a subscription added tomorrow simply starts receiving copies of messages published from then on. Fan-out is why the same event can drive billing, analytics, and email without any of them knowing about the others.

Producerpublishes M once
publish
Topicrecords M, then copies it
fan-out: one copy per subscription
billing / analytics / emaileach gets its own copy of M
One publish fans out to every subscription

Now follow a single copy inside one subscription. It starts pending, meaning it has arrived but nobody is working on it yet. When a consumer pulls it, the copy becomes in-flight: delivered, but not yet confirmed. The broker starts an acknowledgement deadline the moment it hands the copy over. From here, three things can happen.

  • The consumer finishes the work and acknowledges the copy. The broker marks that copy done and never delivers it to this subscription again. This closes the copy for one subscription only; the message stays open on every other subscription until each one acks its own copy.
  • The consumer reports failure with a negative acknowledgement (a nack). The broker puts the copy back into the pending set for redelivery, and remembers that this copy has now been delivered once.
  • The consumer says nothing at all, because it crashed or hung. When the acknowledgement deadline passes, the broker treats the silence exactly like a nack: it assumes the delivery failed and returns the copy to pending.

A redelivered copy goes around the loop again: pending, then in-flight on the next pull, then ack or fail. Every failure increments a delivery counter for that copy. Once the counter passes a configured retry budget, the broker stops retrying and moves the copy to a dead-letter queue, a side channel where poison messages wait for a human or a separate handler to inspect them. Without a dead-letter queue a permanently failing message would redeliver forever and block progress.

  publish
     |
     v
 [pending] <-------------------+
     |                         |  nack / timeout
     |  deliver                |  (budget left)
     v                         |
 [in-flight] --- ack ---> [acked]  (done, never redelivered here)
     |
     |  nack / timeout, budget spent
     v
 [dead-letter]  (parked for inspection)
The lifecycle of one copy in one subscription

The engine on the simulator page is exactly this state machine. Publish a message and watch a copy land in every subscription. Deliver one, let its ack deadline lapse, and watch the same copy get delivered a second time. Nack a copy until its budget runs out and watch it fall into dead-letter, while the other subscription drains cleanly beside it.

At-least-once delivery and why you must be idempotent#

Look closely at the redelivery rule and a consequence falls out. The broker redelivers whenever it is unsure a copy was handled, and a missed ack looks identical whether the consumer crashed before doing the work or crashed just after doing it but before sending the ack. The broker cannot tell those apart, so it errs on the side of delivering again. This is at-least-once delivery: a message is delivered one or more times, and under failures the count is sometimes more than one.

That means a consumer can genuinely process the same message twice. If the handler charges a credit card, a duplicate delivery is a duplicate charge. The fix is not to make the broker perfect, because a broker that guarantees exactly-once across crashes is either very expensive or subtly lying. The fix is to make the handler idempotent: handling the message twice must land the same result as handling it once. In practice you give each message a stable id and record the ids you have already applied, so a repeat is recognised and skipped.

PredictA consumer pulls a message, charges the card successfully, then crashes before its acknowledgement reaches the broker. What happens next, and what should the handler have done?

Hint: The broker cannot tell 'crashed before the work' from 'crashed after the work but before the ack'.

The acknowledgement deadline passes, the broker sees no ack, and it redelivers the same message to a replacement consumer, which charges the card a second time. The broker did nothing wrong; this is at-least-once delivery working as designed. The handler should have been idempotent: record the order id as charged inside the same transaction as the charge, so the redelivery sees the id already applied and skips it.

The opposite setting exists too. At-most-once delivery acks the message before doing the work, so a crash loses it and it is never retried. That trades duplicates for lost messages, which is fine for a metric you can afford to drop and wrong for a payment. Most durable pub/sub systems default to at-least-once because losing a message is usually worse than handling one twice, and idempotency is a tractable thing to build.

What it costs: storage, amplification, and the ack window#

Pub/sub does not have an interesting big-O; its costs are about storage and multiplicity. The first cost is fan-out storage. The broker must track delivery state per subscription, so a single message with N subscriptions produces N pieces of bookkeeping, and each subscription retains its copy until it acks. A topic with ten subscriptions and one slow consumer keeps that consumer's entire backlog resident while the other nine are empty.

The second cost is delivery amplification. Every redelivery is another delivery event, so a subscription with flaky consumers can deliver far more times than the publish count. The gap between total deliveries and total publishes is a direct read on how much retrying is happening, and a large gap is a signal that a consumer is failing rather than that traffic is high.

The third cost is the acknowledgement window. While a copy is in-flight it is invisible to other pulls on that subscription, so a long ack deadline protects slow work from spurious redelivery but also lengthens how long a truly-crashed consumer's message sits stuck. A short deadline recovers from crashes fast but risks redelivering work that was merely slow. Tuning this window is the main knob operators actually turn.

QuantityRoughlyWhy it matters
Bookkeeping per messageO(subscriptions)Each subscription tracks its own copy independently
Total deliveriespublishes + redeliveriesRedeliveries amplify work; the excess signals failing consumers
In-flight windowack deadline per copyToo short redelivers slow work; too long strands crashed work
Retry budgetsmall constant (often 5 to 10)Bounds retries so a poison message dead-letters instead of looping

Trade-offs: the dials you actually set#

  • Delivery guarantee. At-least-once (duplicates possible, nothing lost) is the durable default. At-most-once (no duplicates, loss possible) suits droppable data. Exactly-once is offered by some systems within a bounded scope, but it is costly and never free across arbitrary side effects, so idempotency remains the honest answer.
  • Push versus pull. In pull mode consumers ask for messages when they have capacity, which gives them natural backpressure. In push mode the broker delivers to an endpoint, which is simpler but can overwhelm a slow consumer unless the broker also throttles.
  • Ack deadline length. Longer deadlines tolerate slow work and reduce spurious redelivery; shorter deadlines recover from crashes faster. Systems let you extend the deadline for a message you are still working on rather than force one fixed value.
  • Retry budget and dead-lettering. A generous budget survives transient hiccups; a tight budget quarantines poison messages sooner. Set it with a dead-letter queue attached so exhausted messages are inspected, not silently dropped.
  • Ordering. Strict per-key ordering limits how much you can process in parallel, because you cannot advance past a stuck message in that key. Most pub/sub systems make ordering opt-in for this reason.

How it compares: pub/sub, work queue, and log#

Three patterns are easy to confuse because all three move messages from producers to consumers. The difference is who receives a given message and how progress is tracked.

PatternWho gets a messageHow progress is trackedRedelivery
Pub/sub (this page)Every subscription gets its own copyPer-message state per subscription (pending / in-flight / acked / dead)Per copy, on nack or ack timeout
Work queue (competing consumers)Exactly one consumer in the poolPer-message visibility while a worker holds itOn visibility timeout or nack
Log (Kafka-style)Every consumer group reads the same logA single committed offset per group, a cursor into the logBy resetting the offset backwards

The log is the closest cousin and the sharpest contrast. A log tracks a group's progress with one number, the committed offset, which is a bookmark that only moves forward. That number cannot express "message 5 is done but message 4 is still being retried," because an offset commits a contiguous prefix. Pub/sub tracks each message's state separately, so out-of-order acks and per-message redelivery are natural. The log wins on raw throughput and replay; pub/sub wins on per-message delivery semantics and independent per-consumer retries. Kafka's own internals (its partitioned, replicated log) are covered separately; this page is deliberately the delivery-semantics layer that sits above such a log.

In the real world

The pattern shows up under many names, but the lifecycle is the same one modeled here.

  • Google Cloud Pub/Sub is the pattern almost verbatim: a topic, many subscriptions, at-least-once delivery, an acknowledgement deadline (10 seconds by default, extendable up to 10 minutes), and a dead-letter topic after a configurable maximum delivery attempts, which defaults to 5 and can range up to 100. Its delivery-attempt count is defined as one plus the number of nacks and ack-deadline expirations, exactly the counter used here.
  • AWS models fan-out by pairing two services: an SNS topic fans a message out to several SQS queues, and each SQS queue is then a work queue for one consumer group. SQS uses a visibility timeout (30 seconds by default, up to 12 hours) as its ack window, and moves a message to a dead-letter queue once its receive count passes maxReceiveCount. AWS states plainly that SQS is at-least-once and that consumers must tolerate duplicates.
  • RabbitMQ separates the exchange (the fan-out router) from queues (the per-consumer mailboxes), and uses consumer acknowledgements with requeue-on-nack in the same shape.
  • Redis pub/sub is the deliberate opposite: it is fire-and-forget with no persistence, no acks, and no redelivery, so a subscriber that is offline when a message is published simply misses it. It is a useful reminder that the acking machinery above is exactly what buys you durability.
Common pitfalls
  • Assuming exactly-once. The default is at-least-once, so a non-idempotent handler will eventually double-charge, double-email, or double-count. Design for duplicates from day one.
  • An ack deadline that is too short. If real processing sometimes takes longer than the deadline, the broker redelivers work that was merely slow, and you get duplicates and wasted effort under load. Extend the deadline for long jobs instead of shortening it for everyone.
  • No dead-letter queue. Without one, a message that always fails redelivers forever, burns capacity, and can stall a subscription behind it. Always attach a dead-letter queue and alert on it.
  • Acking before the work is done. Acking on receipt turns at-least-once into at-most-once by accident, and a crash then silently loses the message. Ack only after the work is durably committed.
  • Expecting order without asking for it. Fan-out and redelivery mean copies can arrive out of order across subscriptions and even within one. If you need order, turn on ordering explicitly and accept the reduced parallelism.
  • Letting one slow subscription hide a problem. Because subscriptions are independent, a backed-up one does not slow the others, which is good, but it also means its growing backlog is easy to miss. Monitor per-subscription in-flight and backlog, not just topic-wide totals.
In an interview

If you are asked to design a system where one event triggers several independent reactions (an order fanning into billing, analytics, and notifications, say), reach for pub/sub and be ready to defend the delivery semantics.

  • Lead with the decoupling: the producer publishes once to a topic and does not know its consumers, so you add or remove reactors without touching the producer.
  • Explain fan-out plus per-subscription state: each subscription gets its own copy and tracks it independently, so a slow or failing consumer never blocks the others.
  • State the guarantee out loud. Say at-least-once, then immediately say idempotency, because the interviewer is listening for whether you know duplicates are possible.
  • Mention the ack deadline and the dead-letter queue as the two operational knobs, and be able to say what each does under a crash and under a poison message.
  • Contrast with a log when pushed: an offset commits a contiguous prefix and cannot express per-message retries, whereas pub/sub tracks each message separately. That is the crisp reason to pick one over the other.
Is pub/sub the same as a message queue?

No. A plain work queue delivers each message to exactly one consumer in a pool (competing consumers). Pub/sub delivers a copy of each message to every subscription. The AWS pattern of an SNS topic feeding several SQS queues is literally pub/sub fan-out (SNS) composed with per-queue work queues (SQS).

If a message is acked on one subscription, do the others still get it?

Yes. An ack closes only that subscription's copy. Every other subscription keeps its own copy and its own progress until it acks, nacks, or dead-letters it. That independence is the defining property of the pattern.

Can I get exactly-once delivery?

Some systems offer a bounded exactly-once mode, but it is expensive and cannot cover arbitrary external side effects like a third-party charge. The durable, honest answer is at-least-once delivery plus idempotent handlers keyed on a stable message id.

What ends up in the dead-letter queue?

A message copy that failed (via nacks or ack-deadline timeouts) more times than the retry budget allows. It is parked there so a human or a separate handler can inspect it, instead of retrying forever and blocking the subscription.

Summary#

TL;DRthe 30-second version
  • A producer publishes a message once to a topic; the broker copies it into every subscription. The producer never knows its consumers.
  • Each subscription tracks its own copy through pending, in-flight (delivered but unacked), acked, or dead-lettered. Progress is independent per subscription.
  • An ack closes a copy for one subscription only. A nack or a missed-ack timeout returns the copy for redelivery; past the retry budget it is dead-lettered.
  • Delivery is at-least-once, so a consumer can see the same message more than once. Handlers must be idempotent, keyed on a stable message id.
  • The two operational knobs are the acknowledgement deadline (the in-flight window) and the retry budget plus its dead-letter queue.
  • A log tracks a group's progress with one forward-only offset; pub/sub tracks each message separately, which is what makes per-message retries and out-of-order acks possible.
References
References

Feedback on this topic →