Skip to main content

Publish-Subscribe vs Point-to-Point Queues

What This Concept Is

Two fundamentally different delivery models sit under every messaging system:

Point-to-point (queue)

One message, one consumer. N workers pull from the same queue; whichever worker grabs the message first gets it; the others do not see it. This is work distribution -- a shared to-do list.

                         +---- [worker A]  (got msg 1)
|
producer --> | msg 1 | --+---- [worker B] (got msg 2)
| msg 2 | |
| msg 3 | +---- [worker C] (got msg 3)

Canonical form: AMQP queue, SQS standard queue, JMS queue. One consumer per message; horizontal scale is adding workers.

Publish-subscribe (topic)

One message, every subscriber gets its own copy. This is broadcast -- tell anyone who cares.

                                +---> [subscriber: billing]    (copy of msg 1)
|
producer --> (topic: orders) --+---> [subscriber: inventory] (copy of msg 1)
|
+---> [subscriber: analytics] (copy of msg 1)

Canonical form: JMS topic, AMQP fanout exchange, SNS topic, Kafka topic with separate consumer groups (see Concept 09). Each subscriber gets every message independently; horizontal scale is adding subscribers without touching producers.

Why It Matters Here

Choosing between these two is the most consequential decision you make per message. Picking wrong produces specific failure modes:

  • Pub-sub where you meant work distribution -> every subscriber does the same thing. You charge the card N times, you send N emails. Consumers end up inventing "idempotency keys" to fake queue semantics.
  • Queue where you meant broadcast -> only one of the services that needs to react ever sees the message. Debugging the "missing" reactions takes weeks because nothing is obviously broken: the queue says "delivered."

The intent (Cluster 1) maps cleanly to the model:

  • commands -> point-to-point queues (exactly one handler)
  • events -> publish-subscribe topics (zero to many subscribers)

Concrete Example

Use point-to-point for commands

API ---> queue "payments.charge" ---> [worker 1 of 10]
[worker 2 of 10]
...

ChargeCard(order_id, amount, card_token) is a command. Exactly one worker should charge exactly once. Scale horizontally by adding workers; they share the queue.

Use pub-sub for events

payment service --> topic "payment.captured"
|
+---> [ledger] (records the transaction)
+---> [notify] (emails the receipt)
+---> [analytics] (updates dashboards)
+---> [loyalty] (awards points)

PaymentCaptured(order_id, amount) is a fact. Four services each get a full copy. Adding fraud-analytics next month is a new subscription; nothing about the payment service changes.

The hybrid that bites you

Many brokers let you do "queue groups inside a topic" (Kafka consumer groups, NATS queue groups). That is both models at once:

  • across consumer groups -> pub-sub (each group gets every message)
  • inside a consumer group -> point-to-point (one consumer per message)

This is powerful but often misused. A team runs one "consumer group" per service, which gives them pub-sub across services and work distribution within each service. This is the right default -- but only if you remember you are combining the two models.

Common Confusion / Misconception

"SQS is pub-sub." No. SQS is a queue; one consumer gets each message. To broadcast, you put an SNS topic in front and fan out to multiple SQS queues. The AWS convention is "SNS for pub-sub, SQS for point-to-point."

"Kafka is pub-sub." Kafka is a log with both models available through consumer groups. One consumer group = work distribution (partitioned). N consumer groups = pub-sub (each group reads independently).

"If we have 10 subscribers, pub-sub is better." Not if each subscriber is just a worker of the same logical task. Ten workers processing the same message ten times is a bug, not an architecture.

"Queues are old-fashioned and log-based systems replace them." They solve different problems. Queues excel at task distribution with at-least-once delivery and DLQs. Logs excel at replayable event streams and multiple independent consumers at different speeds. See Concepts 07 and 08.

How To Use It

Picking the model, per message:

Naming conventions that make the model obvious to the next engineer:

ConventionExampleIntent
*.commands / cmd.*payment.commands.chargePoint-to-point
Past-tense topicorder.placed, payment.capturedPub-sub
*.dlqpayment.commands.charge.dlqDead-letter for either model

Check Yourself

  1. Name one message type that must be point-to-point and one that must be pub-sub. Defend each in a sentence.
  2. A team has ten "consumers" of user-signed-up -- they turn out to all be doing the same "send welcome email" with slight variations. Queue or topic? Why?
  3. What happens to an SNS message that has no subscribers when it is published?

Mini Drill or Application

For a checkout workflow (order placed -> charge -> reserve inventory -> ship -> notify), in 15 minutes:

  1. List the 6-8 messages you would send.
  2. Classify each as point-to-point or pub-sub.
  3. Name the topic or queue explicitly with a naming convention you chose.
  4. Mark the DLQ strategy for each.

Transfer to Adjacent Domains

  • Cluster 3 (brokers). Pub-sub vs point-to-point is a semantic choice; queues vs log-based brokers is a substrate choice. Kafka offers both (across consumer groups = pub-sub, within a group = point-to-point). Clarifying intent in this concept tells you what to configure in the next cluster.
  • Cluster 4 (saga choreography). A choreographed saga is built from pub-sub events and point-to-point command queues braided together. Mis-modelling a "command as a broadcast event" is one of the two leading saga-failure patterns.
  • Scalability (S8M1). The two models imply different scale-out levers: pub-sub scales by adding subscribers (each with its own pipeline); point-to-point scales by adding workers behind one queue. Autoscaling rules differ accordingly -- do not wire one set of CloudWatch alarms for both.
  • Webhooks / SaaS integrations. External webhooks are fundamentally pub-sub over HTTP. The same "many independent subscribers" semantics apply; the substrate is the HTTP delivery+retry loop rather than a broker.

Read This Only If Stuck