Skip to content

RabbitMQ Messaging Patterns: Queues, Competing Consumers, and Fanout

Core Concept LearningJuly 16, 20265 min readUpdated July 21, 2026

RabbitMQ is a broker: producers publish messages; exchanges route them; queues buffer work; consumers process and acknowledge. The same building blocks combine into different patterns — a single worker queue, a pool of competing workers, or a fan-out where every subscriber gets a copy. See event-driven architecture patterns for the larger producer, consumer, and outbox context.

Infographics sometimes label a lone producer→queue→consumer path as “publish-subscribe.” That label is misleading. In AMQP/RabbitMQ terms that path is point-to-point (one message delivered to one consumer). True multi-subscriber broadcast usually means a fanout (or topic) exchange bound to many queues. This guide maps the correct vocabulary, then shows the three patterns you actually draw on whiteboards. For broker choice vs Kafka/SQS, see Kafka vs RabbitMQ vs SQS.

Knowledge map: Producer → Exchange → Queue → Consumer → ack
Knowledge map: Producer → Exchange → Queue → Consumer → ack

RabbitMQ knowledge map

Hold four concepts: Producer emits a message to an exchange. The exchange applies a routing rule and places copies (or a single copy) into one or more queues. A consumer pulls from a queue and acks when processing succeeds. Without an exchange in the picture, tutorials often hide the default exchange — but production mental models should keep it visible.

Philosophy: queues protect consumers from bursty producers; exchanges decide who should see a message. Delivery is typically at-least-once unless you design otherwise — consumers must be idempotent. Dead-letter exchanges catch poison messages; they are not optional garnish for serious systems.

Quick reference

  • Definition: exchange routes; queue stores; consumer processes + acks.
  • Heuristic: one queue → competing consumers share work (each message once).
  • Heuristic: fanout/topic → each bound queue gets a copy (broadcast / pub-sub style).
  • Product default: default exchange can route by queue name — fine for demos, opaque in reviews.
  • Compare brokers: see Kafka vs RabbitMQ vs SQS for replay and ops trade-offs.

Remember this

The exchange decides who sees a message; the queue only decides when a consumer sees it — a dead-letter exchange isn't optional garnish once delivery is at-least-once and consumers must handle redelivery.

Simple queue (point-to-point)

One producer publishes; one queue holds messages; one consumer processes them in order (for that consumer). This is the pattern many slides misname “publish-subscribe.” Mechanically it is work hand-off: each message is consumed once. Use it when a single worker (or a future pool) owns a task type — email send, thumbnail job, webhook delivery.

Consequence: throughput is capped by that consumer. If it dies without ack, the message can be redelivered. If it is slow, the queue grows. Scaling means adding competing consumers on the same queue — the next pattern — not pretending one consumer is a broadcast bus.

Point-to-point: Producer → one Queue → one Consumer (not pub-sub)
Point-to-point: Producer → one Queue → one Consumer (not pub-sub)

Quick reference

  • Slide correction: producer → queue → consumer is point-to-point, not multi-subscriber pub-sub.
  • Best for: single-worker tasks, ordered processing by one consumer, getting started.
  • Ack after successful work; nack/requeue carefully to avoid hot loops.
  • Prefetch (QoS) limits unacked messages in flight to one consumer.
  • When to skip: many independent subscribers each need their own copy — use fanout/topic.
Ack before work (loses jobs on crash)
1// amqplib-style sketch2await channel.assertQueue("email", { durable: true });3channel.consume("email", (msg) => {4  if (!msg) return;5  channel.ack(msg);6  sendEmail(JSON.parse(msg.content.toString())); // crash → already acked7});
Ack after success; nack failures
1await channel.assertQueue("email", { durable: true });2channel.consume("email", async (msg) => {3  if (!msg) return;4  try {5    await sendEmail(JSON.parse(msg.content.toString()));6    channel.ack(msg);7  } catch (err) {8    channel.nack(msg, false, !isPoison(err)); // false requeue → DLX when set9  }10});

Remember this

Acking before the work runs loses the job on a crash — ack only after success, since a simple queue delivers each message once to one consumer, not to every subscriber the way a slide's "pub-sub" label implies.

Competing consumers

Several consumers attach to the same queue. RabbitMQ delivers each message to one of them (round-robin by default, modulated by prefetch and ack timing). Producers still publish once; workers share the load. This is the classic work queue / competing consumers pattern.

Mechanism: scale horizontally by adding consumers — no new queues required. Trade-off: ordering across the whole stream is no longer trivial; sticky per-key ordering needs careful design (or a different tool). Failed consumers that never ack can stall a message until timeout/requeue. Philosophy: compete for work units, not for “who hears the event.”

Competing consumers: one shared queue fans out to alternative workers
Competing consumers: one shared queue fans out to alternative workers

Quick reference

  • Each message goes to exactly one consumer on that queue (under normal competing delivery).
  • Best for: parallel workers on the same job type (resize, OCR, email blast chunks).
  • Tune prefetch so one slow consumer does not hoard all messages.
  • Idempotency still required — redelivery happens.
  • When to skip: every service must react to the same event — that is fanout/topic, not compete.
Unlimited prefetch (one slow worker hoards)
1// Two workers on "resize" — worker A prefetches everything2channel.prefetch(0); // no limit3channel.consume("resize", handler);
Prefetch + shared queue
1// Both workers: fair dispatch2channel.prefetch(1);3channel.consume("resize", async (msg) => {4  if (!msg) return;5  try {6    await resizeImage(JSON.parse(msg.content.toString()));7    channel.ack(msg);8  } catch {9    channel.nack(msg, false, true); // requeue for another worker10  }11});12// Second process: same queue name → competes for messages

Remember this

Unlimited prefetch lets one slow worker hoard every message on the queue — set prefetch to 1 so competing consumers actually share the load instead of one consumer starving the rest.

Fanout exchange (broadcast)

A fanout exchange ignores routing keys and copies every published message to all bound queues. Each queue has its own consumer (or competing pool). That is RabbitMQ’s straightforward publish-subscribe / broadcast shape: inventory, email, and analytics can each see the same OrderPlaced without the producer listing subscribers.

Mechanism: bind N queues to the fanout; publish once; N copies land. Trade-off: more storage and more processing; slow subscribers need their own queues so they do not block others. Topic and direct exchanges add selective routing — fanout is “everyone who bound.” Truth: Kafka consumer groups are a different model (offset log, replay); do not equate them one-to-one with fanout queues.

Fanout exchange: one publish → copy into every bound queue
Fanout exchange: one publish → copy into every bound queue

Quick reference

  • Fanout = true multi-subscriber copy-per-queue; closest to “pub-sub” on the slide.
  • Best for: domain events many services must observe independently.
  • Bind queues at deploy time (or via management) — producers stay unaware of consumers.
  • Per-queue DLX and retry policies isolate bad subscribers.
  • When to skip: only one worker should handle each message — use one queue + competing consumers.
Publish to one queue (not broadcast)
1await channel.assertQueue("orders");2channel.sendToQueue("orders", Buffer.from(payload));3// Only consumers on "orders" see it — not true multi-service pub-sub
Fanout bind + publish once
1await channel.assertExchange("order.events", "fanout", { durable: true });2await channel.assertQueue("inventory", { durable: true });3await channel.assertQueue("email", { durable: true });4await channel.bindQueue("inventory", "order.events", "");5await channel.bindQueue("email", "order.events", "");6channel.publish("order.events", "", Buffer.from(payload));7// Both queues get a copy; ack failures stay isolated per queue

Remember this

Publishing straight to a named queue reaches only that queue's consumers — a fanout exchange bound to N queues is what makes every service see its own copy without the producer ever listing subscribers.

Choosing the pattern

Decision rule: one worker type sharing jobs → one queue + competing consumers. Many services each need the event → fanout (or topic) + one queue per service. Single consumer prototype → simple queue, then add competitors when load appears.

Operational philosophy: name the property you need — work distribution vs fan-out notification — before naming the exchange type. Pair every production queue with ack discipline, idempotent handlers, and a dead-letter path. For broker choice vs Kafka/SQS, use the comparison guide; this article is about shapes inside RabbitMQ.

Zoom into one RabbitMQ delivery that fails and is retried safely
Zoom into one RabbitMQ delivery that fails and is retried safely

Quick reference

  • Work sharing → competing consumers on one queue.
  • Broadcast → fanout/topic with a queue per subscriber.
  • Point-to-point demo → simple queue (not “pub-sub”).
  • Always: ack, idempotency, DLX, monitoring of queue depth.
  • Practice: implement OrderPlaced once as work queue, once as fanout to two fake services.

Remember this

Name the property first — work distribution or fan-out notification — before naming an exchange type, since one queue with competing consumers and a fanout exchange solve different problems that look similar on a whiteboard.

Key takeaway

RabbitMQ patterns are compositions of producer, exchange, queue, and consumer. A single queue feeds point-to-point or competing workers; a fanout exchange feeds true multi-subscriber copies. Correct the slide when it calls a lone queue “publish-subscribe,” then choose the shape that matches work-sharing versus broadcast.

Practice (20 min): Run RabbitMQ locally (Docker). Publish ten messages to one queue with two consumers (compete)—use the prefetch + ack sketch. Then bind two queues to a fanout and prove both get every message. Write one sentence naming which property each demo protected.

Share:

Related Articles

Decoupling microservices using Event-Driven Architecture (EDA) requires choosing an asynchronous messaging backbone. Eng

Read

In high-concurrency microservices architectures, preventing race conditions when multiple stateless worker instances acc

Read

System design interviews evaluate a candidate's ability to architect scalable, resilient, and cost-effective distributed

Read

Keep learning

Follow a structured path or browse all courses to go deeper.