System Design

Pub/Sub vs Message Queues: Architecture, Delivery Guarantees, and Scaling Patterns

Pub/Sub vs Message Queues decides if many readers share one event or one worker owns a task. Learn delivery rules, scale limits, and common failure modes.

Executive Summary: Pub/sub and message queues answer different questions — does one event fan out to many independent readers, or does exactly one worker own and complete a task — and mixing the two models is how you either lose events or process the same job twice. This guide covers delivery guarantees each pattern actually provides, the scaling behavior under a traffic spike, and picking the right one before a deploy doubles load and workers start falling behind.

Pub/Sub vs Message Queues is the choice that decides who owns a unit of work. You feel that choice when a deploy doubles traffic and workers fall behind. If you mix the two models, you either lose events or run the same job twice. This guide shows when each pattern fits, how delivery rules behave, and what breaks at scale.

What breaks when you pick the wrong model

A queue hands one message to one worker. After that worker acks, the message is gone. A topic copies the same event out to many readers. Each reader keeps its own place in the log.

When you need both effects, you should split the path. Publish the fact first. Then let one reader push a job onto a queue.

In my experience, teams pick a topic because it sounds open. Then they add five readers that all change the same row. As a result, writes race and tickets pile up. A queue would have forced a single owner.

However, a queue hides the event from other readers. If search, billing, and fraud all need the same fact, a topic is the better start.

Production fails in a few repeat ways. First, a reader falls behind and the backlog grows with no page. Second, a worker crashes after the side effect and before the ack. Then the job runs again.

Third, people assume order across all keys. Still, brokers only keep order inside one partition or one message group. Overall, the bug is a contract you never wrote down.

How each model moves work

Both models move bytes through a broker. The split is who is allowed to finish the work. If you get that split wrong, no amount of hardware will save the design. Therefore, name the owner before you name the product.

Topics and many readers

A publisher sends one record to a topic. The broker stores it, often on disk, and gives it an offset. Each subscription reads at its own pace. When billing is slow, search can still be current.

Also, a new reader can start at the latest offset or from a time you choose. Because readers do not compete, you scale by partitions and by reader count per partition. If you add readers past the partition count, some sit idle.

The Google Cloud Pub/Sub overview shows how each subscription tracks ack state. The Apache Kafka documentation explains partitions, offsets, and how long records stay on disk. You should read retention and ack rules before you pick a default.

For example, a short retention window means a reader that was down all weekend loses data. Therefore, set retention to cover your longest incident, not your happy path.

Queues and competing workers

A queue stores work that one worker should finish. Many workers poll, or they take a push. The broker hides a message after delivery for a visibility window. If the worker acks in time, the message leaves.

If the worker dies, the message comes back after the timeout. Since two workers must not run the same job at once, that timeout is a lock. You should size it with the same care you give timeouts and idempotent retries.

The Amazon SQS Developer Guide describes this competing worker pattern. It fits image resize, mail send, and other jobs where one success is enough. Meanwhile, it is a poor fit for a fact that many systems must store.

You would have to copy the payload yourself. Instead, publish the fact on a topic and let one subscriber enqueue the job.

Delivery rules and order

Most hosted queues and logs give at least once delivery. Exactly once is a narrow feature with strict rules. If your handler is not safe to run twice, at least once becomes a double charge or a double mail.

You should store the message id in a table with a unique key. After a retry, the insert fails and you ack. That is the practical form of exactly once.

Order is local. A log keeps order inside a partition. A FIFO queue keeps order inside a message group. A standard queue does not keep order at all.

When you need global order, you also accept one writer and a lower rate cap. Although that can be right for a ledger, it will not serve a flash sale. Instead, order by entity id.

All events for one order share a key. Events for other orders run in parallel.

Duplicates and delay also change what readers see. That is why this choice sits next to eventual consistency in practice. A reader can be minutes behind and still be healthy.

Your product copy and your alerts must allow that gap. If an alert fires on any lag, you will train the team to ignore it.

Trade-offs you should weigh

Use the table as a first filter. Then test the choice against your failure story. If you cannot name who owns the side effect, you are not ready to pick a broker.

Choice.Best fit.Delivery.Main risk.
Topic.Many systems need the same fact.At least once per reader.However, readers can race on one row.
Queue.One worker must finish a job.At least once for the group.Also, a slow worker blocks that copy.
FIFO group.Order matters for one entity.At least once inside the key.Still, a hot key caps your rate.
Log plus queue.You need facts and jobs.Split by hop.Therefore, you must trace both hops.

A topic costs more when every reader stores or receives a full copy. A queue is cheaper when one ack deletes the only copy. However, a queue makes replay harder after that ack.

If you may need to rebuild a search index, keep the log. If you only need to send a mail once, a queue is enough.

Do not use a topic as a work queue unless the product has competing consumers built in. Some logs can do it with a shared group. Others cannot.

When the docs are vague, assume fan out. Then you will not be surprised by five copies of the same charge.

Pitfalls after launch

A common mistake I have seen is to ack before the database commit. The process then dies. The row is missing, and the message is gone.

Because the broker did its job, the bug looks like a lost request. Write the row first, with the message id. Then ack.

Another trap is a poison message. The handler throws on bad data, the message returns, and the worker loops. Meanwhile, healthy messages behind a FIFO key wait.

After a fixed number of tries, route the bad message to dead letter queues. Also, page a human. A silent drop hides data loss.

Backpressure is the third trap. Publishers keep writing while readers are dead. Disk fills, or the bill spikes, or the oldest records expire.

If you do not cap publish rate, the broker becomes your incident. Slow down the publisher when lag crosses a line you set in advance.

When a push subscription calls your service, treat it like a webhooks architecture. Check the signature. Bound the handler time.

Return a clear status so the broker can retry or stop. If you return success before the work is durable, you will lose the event.

Use this order when a queue or topic pages you. Do not skip ahead, because a hot key can look like a full outage.

  1. First, check lag per reader, not only broker CPU.
  2. Then, look for one hot key or one poison body.
  3. Next, confirm acks happen only after the side effect is stored.
  4. Finally, replay from the log or the dead letter path if data was lost.

A config you can start from

The snippet below is a sketch, not a vendor file. It shows the split we use when one fact must fan out and one job must run once. Change the numbers for your own latency budget. If your handler is slower than the visibility window, you will get duplicate runs.

topic: order-events
retention_hours: 168
partitions: 24
subscriptions:
  billing:
    ack_deadline_seconds: 30
  search:
    ack_deadline_seconds: 60
queue: order-jobs
visibility_timeout_seconds: 90
max_receives: 5
dead_letter_queue: order-jobs-dlq
worker:
  handlers: 8
  store_message_id: true

Billing reads the topic and writes a ledger row. It also enqueues a job only when a human must be mailed. Search reads the same topic and never sends mail.

The queue workers compete, so only one of them sends a given mail. Since the message id is stored, a retry does not send a second copy.

Performance, scale, and cost

Scale comes from partitions, batch size, and even keys. Adding machines helps only when work can spread. A single hot tenant will not go faster if every event shares one key.

You should shard that tenant or shed load. Otherwise you will buy CPUs that sit idle.

Retention dominates storage cost on a log. As an illustrative production range, one million messages a day at 1 KB, kept for seven days, is on the order of several gigabytes before replication. Fan out can multiply delivery cost when the bill is per message per reader.

A queue usually stores one copy until ack, so a fast worker keeps the bill small. However, a stuck worker keeps every message and the bill grows.

Cross zone traffic is easy to miss. If publishers and readers sit in different zones, you pay for each hop. Also, a topic with ten readers can copy that traffic ten times.

Place readers near the broker, and batch reads. For example, a batch of one hundred records cuts request overhead a lot, though it adds a little delay.

Lag is the metric that matters. CPU can look fine while one reader is hours behind. Page on lag age and on dead letter depth. Still, do not page on a short blip.

Set the line above your normal batch delay. We once hit a bottleneck when a nightly job reset offsets by mistake. The replay looked like a traffic spike. A change ticket and a lag alert would have caught it in minutes.

Cost also tracks retries. A handler that times out at the edge of the visibility window doubles work. Therefore, measure handler time at the high percentile, not the average.

Then set the window above that number with some slack. If the slack is huge, a crash hides the message for too long and the queue looks empty.

Key Takeaways

  • Also, use a topic when many systems must see the same fact.
  • However, use a queue when one worker must own the side effect.
  • Therefore, treat at least once as the default and make handlers safe to retry.
  • Still, keep order only inside one key, unless you accept a low rate cap.
  • Because acks are final, write the side effect before you ack.
  • After repeated failures, park the message on a dead letter path and page a human.
  • Finally, scale with partitions and even keys, and watch lag age rather than CPU alone.

FAQ

When should you choose a queue over a topic?

Choose a queue when one success is enough and a second copy would harm a user. Mail, charges, and file transforms fit this rule. If many products must store the same fact, start with a topic. Then enqueue a job only for the single owner step.

What happens if a worker acks too early?

The broker deletes or commits the message. If the process dies before the write lands, the work is gone. Write a durable row with the message id first. Then ack.

How should you handle order when one key is hot?

When one tenant or one sku takes most of the traffic, a single group becomes the cap. Split that key into shards if the business rules allow it. Otherwise shed load or slow that publisher. Adding workers will not help while they all wait on one group.

Can you get exactly once if the handler writes to a database?

You can get the effect users need if the write is unique on the message id. A retry hits the same key and becomes a no op. The broker may still deliver twice. If the side effect is an external call with no id, you do not have exactly once.

Next, pick one production flow and label each hop as a topic or a queue. Write down who owns the side effect, how long you keep the record, and what you do after five failures. Then load test a hot key before you trust the design. If the labels are unclear, fix the contract before you add another broker feature.

Last updated on 06 September 2026.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *