Message Brokers, Queues & Integration Patterns

The state machines behind work queues and publish-subscribe: routing, publisher confirms, consumer acknowledgements, redelivery, ordering, backpressure, retries, dead letters, and the integration patterns that turn broker primitives into reliable application workflows.

builds onBackpressure & Load Shedding

A broker is not merely a network pipe between two processes. It is a state machine that decides where a message belongs, who currently owns it, when that ownership expires, and when the broker may forget it. Most production surprises come from leaving one of those decisions implicit.

The first design question is therefore not “RabbitMQ or Kafka?” It is whether the application needs a work queue whose delivery state is owned by the broker, a retained log whose read positions are independent state, or a combination. This entry develops the queue side. Log-Based Messaging & Streaming develops the other.

The broker state machine #

In the AMQP 0-9-1 model used by RabbitMQ, publishers do not write directly to queues. They publish to an exchange; bindings tell the exchange which queues receive a copy; consumers receive deliveries from a queue. The exchange routes but does not retain messages. Each destination queue owns its copy and its delivery state independently.

Two independent responsibility transfers Publisher message id: m-42 Topic exchange billing.commands ready audit.events ready Billing worker DB commit then acknowledge Audit consumer append evidence then acknowledge 1 publish 2 routed copies 3 publisher confirm: broker accepted responsibility 4 delivery tag 91 4 independent copy 5 ack after commit 5 ack after append crash or ack timeout → redelivery A confirm does not prove consumption. An acknowledgement does not prove the publisher's business request completed. End-to-end completion needs domain state; at-least-once delivery needs idempotent effects.
Routing creates independent queue copies. Confirms and acknowledgements close different failure windows; neither proves the final business effect by itself.
message path publisher confirm consumer ack after durable effect

There are two independent responsibility transfers:

  1. Publisher → broker. A socket write proves only that bytes entered a local kernel buffer. A publisher confirm says that RabbitMQ accepted the publish according to the destination's durability rules. For a quorum queue, the confirm is issued only after a quorum of replicas accepted the message. A confirm does not say that any consumer saw or processed it.
  2. Broker → consumer. A consumer acknowledgement says that the application accepts responsibility for the delivery. The safe point is after the intended durable side effect commits, not when the handler starts and not merely when bytes arrive. An acknowledgement knows nothing about the original publisher.

Those signals are deliberately orthogonal. End-to-end business completion needs a domain acknowledgement or a queryable state transition of its own. Publisher confirms plus consumer acknowledgements provide the machinery for at-least-once processing; they do not make an arbitrary database update, email, and payment exactly once.

Channels, queues, and subscriptions are different objects #

A message carries an opaque body plus metadata. A channel is the logical path over which messages travel. A queue stores messages and assigns deliveries to consumers. A subscription is durable interest in a source; in queue-oriented brokers it is often implemented as a queue plus a binding.

This distinction explains the two common topologies:

Intent Topology Ownership consequence
Divide work among workers one queue, many competing consumers each queued copy is assigned to one consumer at a time
Let independent services observe the same event one source, one queue/subscription per service routing creates an independently acknowledged copy per service

Adding three consumers to one queue does not broadcast each message three times; it divides work. Conversely, binding three queues to a fanout or topic exchange creates three independently retained copies. This is the key design difference between “scale one logical subscriber” and “add another logical subscriber.”

AMQP's exchange types express common routing policies:

Exchange Match Typical use
direct binding key equals routing key named command or work class
fanout every binding matches broadcast to independent queues
topic dot-separated key with * and # wildcards event categories such as orders.*.failed
headers selected header values match routing on typed metadata rather than one string key

The default RabbitMQ exchange is still a direct exchange: every declared queue is automatically bound with its queue name as routing key. Publishing “to a queue” is therefore convenient syntax over the same exchange/binding model, not a separate transport path.

Delivery semantics are failure policies #

The familiar labels are only meaningful after naming the boundary:

Policy Failure behavior Application consequence
at-most-once acknowledge before work, or do not retry ambiguous sends loss is possible; duplicate processing is avoided only by dropping uncertainty
at-least-once retain or resend until an acknowledgement arrives duplicates are possible; handlers must be idempotent
exactly-once claim deduplicate and atomically couple a narrowly defined read/write boundary inspect the scope; external effects usually remain outside it

Consider a worker that charges a card and then crashes before acknowledging. The broker cannot distinguish “charge committed, ack lost” from “charge never happened,” so it redelivers. Suppressing that retry would lose work in the second case. The broker cannot solve the ambiguity alone; the handler needs a stable message or command id and a durable idempotency record in the same transaction as the protected state change.

Idempotentsql
BEGIN;

INSERT INTO handled_messages (consumer, message_id)
VALUES ('billing', :message_id)
ON CONFLICT DO NOTHING;

-- Continue only when the insert affected one row.
UPDATE invoices
SET status = 'paid'
WHERE id = :invoice_id AND status = 'pending';

COMMIT;
-- Acknowledge the delivery only after COMMIT succeeds.

This protects the database transition. An email or remote API call still needs its own idempotency key or an outbox/inbox boundary.

Ordering ends where parallel work begins #

A queue can dequeue in FIFO order while the application completes out of order. If worker A receives message 41 and pauses while worker B finishes 42, the observable side effects are 42 then 41. Prefetch makes this window larger: messages already delivered but not acknowledged are no longer available to other workers, even though they have not completed.

RabbitMQ's strongest simple ordering path is one publishing channel → one exchange → one queue → one active consumer, without priorities or requeueing. Multiple publishers can interleave; priorities intentionally reorder; redelivery can move an older message relative to newer work; multiple active consumers preserve dequeue order but not completion order.

When the domain requires order per entity but also needs parallelism, partition the work by a stable key such as order_id across several queues, then use one active consumer per partition. The key becomes a durable contract: changing the partition count or hash function can move an entity and overlap old and new workers, so migration needs an explicit drain or epoch boundary.

Prefetch is the local backpressure valve #

RabbitMQ normally pushes deliveries. Prefetch limits the number of unacknowledged deliveries assigned to a consumer. Too low and the worker starves while acknowledgements travel back; too high and one slow worker hoards work, increases redelivery bursts after a crash, and hides backlog in client memory rather than in observable queue depth.

For average processing time SS seconds and desired per-worker concurrency cc, a first approximation is prefetch near cc, then measure. The system has at least three distinct inventories:

Ntotal=Nready+Nunacked+Nretry/dead−letter.N_{total}=N_{ready}+N_{unacked}+N_{retry/dead-letter}.

Watching only ready messages can therefore report a healthy queue while thousands of deliveries are stuck unacknowledged. Pair queue depth with oldest message age, unacknowledged count, redelivery rate, handler latency, and publisher-confirm latency. The broader capacity model belongs in Backpressure & Load Shedding.

Retries are a scheduled control loop #

Immediate negative-acknowledge-and-requeue is often a positive feedback loop: the dependency is down, every worker fails instantly, and the same messages consume all broker and application capacity. A durable retry design includes:

  • a bounded attempt count recorded in trustworthy metadata;
  • exponential or otherwise increasing delay with jitter;
  • classification of transient, permanent, and unknown failures;
  • a separate retry schedule or delayed queue so fresh work is not starved;
  • idempotency across every attempt;
  • an operator-visible terminal state.

A dead-letter queue is quarantine, not recovery. It preserves evidence after expiry, rejection, or attempt exhaustion, but it does not decide whether the payload is poison, repair it, or safely replay it. Re-drive must preserve the original id, record the operator action, and avoid resetting an infinite retry cycle. RabbitMQ dead-lettering is itself a publish operation and can fail; default dead-letter transfer is not automatically lossless. Quorum queues provide an explicit at-least-once dead-letter mode using internal publisher confirms, with its documented constraints.

Enterprise Integration Patterns are compositions, not products #

Enterprise Integration Patterns is useful because it names recurring roles independently of RabbitMQ, Kafka, SQS, or a framework. The 65-pattern catalog is best understood as six questions:

Family Design question Representative patterns
message construction what does the message mean and how are conversations related? Command Message, Event Message, Correlation Identifier, Message Sequence, Expiration
channels which delivery relationship exists? Point-to-Point Channel, Publish-Subscribe Channel, Dead Letter Channel, Message Bus
routing which recipients and processing path apply? Content-Based Router, Recipient List, Splitter, Aggregator, Resequencer, Scatter-Gather
transformation how do incompatible representations meet? Translator, Envelope Wrapper, Content Enricher, Normalizer, Canonical Data Model
endpoints how does an application safely attach? Polling Consumer, Event-Driven Consumer, Competing Consumers, Durable Subscriber, Idempotent Receiver
management how is the invisible flow observed and controlled? Control Bus, Wire Tap, Message History, Message Store, Test Message

The patterns are a vocabulary, not a mandate to insert a broker or an enterprise service bus everywhere. A Content-Based Router implemented as a pure function inside one service is still the same design role. Distributing it adds independent failure, deployment, and observability boundaries; that cost must buy something concrete.

Worked flow: order fulfillment #

Suppose orders.accepted contains a list of line items:

  1. A Message Translator converts the producer schema into the fulfillment contract and preserves message_id, order_id, causation, and trace ids.
  2. A Splitter emits one reservation command per line with sequence size and position metadata.
  3. Competing inventory workers act as Idempotent Receivers keyed by the command id.
  4. An Aggregator correlates replies by order_id, persists partial state, and completes when all expected positions arrive or a deadline expires.
  5. A Content-Based Router sends complete orders to shipping and incomplete ones to compensation.
  6. A Wire Tap copies lifecycle events to observability without becoming a dependency of the command path.

The hard parts are state and time: where the aggregator's partial state lives, what duplicate replies do, how late replies are classified, and what happens after the timeout. Merely drawing Splitter → Aggregator does not answer them.

Request-reply over a broker #

Messaging can emulate RPC by placing a reply address and correlation id in the request. That does not make the call non-blocking from the caller's point of view: the caller still needs a deadline, cancellation policy, bounded reply state, duplicate-response handling, and behavior for a late response after the request has timed out.

Use brokered request-reply when routing, temporal decoupling, buffering, or auditable commands are requirements. For a simple low-latency query where the caller cannot proceed without the answer, ordinary RPC is usually more honest and easier to operate.

Queue implementations and near alternatives #

Products overlap, so compare the semantics selected by configuration rather than the logo:

System / mode State model Best fit Important boundary
RabbitMQ quorum queue routed queue; replicated ownership and ack state reliable commands and work distribution with rich routing confirms and manual acks are required to benefit from quorum durability
RabbitMQ stream retained append-only log with offsets replay and large backlogs inside the RabbitMQ ecosystem not a traditional queue; several queue features such as DLX do not apply
ActiveMQ Artemis addresses, queues, anycast/multicast routing; multiple protocols JMS/AMQP environments and conventional broker workloads protocol-facing names map onto Artemis's own address model
Amazon SQS managed pull queue with visibility leases operationally simple background work Standard queues are at-least-once and best-effort ordered; delete closes the lease
Google Cloud Pub/Sub topic plus independently retained subscriptions managed fanout and elastic subscribers default delivery is at-least-once and unordered; ordering/exactly-once have explicit scopes
Kafka / Redpanda / Pulsar retained partitioned logs and subscription positions replayable event history and stream processing partition key and retention are data-model decisions, not tuning details

RabbitMQ classic mirrored queues, discussed in older comparisons, are not a current high-availability recommendation: they were deprecated in 2021 and removed in RabbitMQ 4.0. Quorum queues are the replicated queue primitive; streams are the retained-log primitive. This is one reason a 2018 product comparison should not be used as an operational guide today.

Design review checklist #

Before choosing or shipping a broker topology, answer these explicitly:

  • Is each message a command, fact, document, or reply? Who owns its schema?
  • Does each logical subscriber need an independent copy, or are consumers dividing one workload?
  • What exact event allows the publisher to forget the message?
  • What durable effect must commit before the consumer acknowledges?
  • Which duplicate ids and idempotency records survive process and broker restarts?
  • Which ordering boundary matters, and what concurrency preserves it?
  • Where are ready, unacknowledged, delayed, and dead-lettered inventories measured?
  • Which failures are retried, after what delay, how many times, and by whom?
  • Can unroutable publishes and failed dead-letter transfers disappear?
  • How are schema evolution, correlation, causation, trace context, sensitive data, retention, and deletion governed?
  • Is replay required? If yes, a queue may be the wrong source of truth.

If any answer is “the broker handles it,” the design is still underspecified.

Sources

  1. docsAdvanced Message Queuing Protocol Specification, Version 0-9-1pdfnotes
  2. bookEnterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutionsnotes
  3. docsRabbitMQ — Exchanges
  4. docsRabbitMQ — Consumer Acknowledgements and Publisher Confirms
  5. docsRabbitMQ — Quorum Queues
  6. docsRabbitMQ — Dead Letter Exchanges
  7. docsAmazon SQS — Standard Queues
  8. docsGoogle Cloud Pub/Sub — Subscription Overview
  9. docsApache ActiveMQ Artemis — Address Model

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close