Steven's Knowledge

RabbitMQ

RabbitMQ in depth - exchanges, queues, bindings, dead-letter queues, AMQP, clustering, operational notes

RabbitMQ

RabbitMQ is a mature, AMQP-based message broker. Producers publish to exchanges; exchanges route to queues via bindings; consumers take messages off queues and acknowledge them. Compared to Kafka, RabbitMQ is routing-first rather than retention-first.

Core Vocabulary

TermWhat it is
ConnectionTCP connection to the broker
ChannelLightweight virtual connection inside a Connection; what you use
ExchangeReceives messages from publishers and routes them
QueueBuffer that holds messages until consumed
BindingThe rule connecting an exchange to a queue
Routing keyA string attached to each message; exchanges use it
Vhost (virtual host)Logical namespace; permissions and resources isolated
ConsumerSubscribes to a queue, gets messages pushed to it
Ack / NackConsumer confirms (or rejects) successful processing
DLX (dead-letter exchange)Where messages go after they're rejected or expire

Exchanges: Four Routing Strategies

TypeRoutes byUse case
directExact routing key matchTask queues, point-to-point
topicPattern match (order.*, #.error)Selective event routing — most flexible
fanoutBroadcast to all bound queuesNotifications, cache invalidation, pub/sub
headersMatch on message header valuesRare; complex multi-dimensional routing

Direct Exchange

Publisher → [exchange:direct] ──"images"──► images-queue   → image worker
                              └"emails"──► emails-queue   → email worker
                              └"reports"─► reports-queue  → report worker

Each routing key sends to a specific queue. Useful for "this task type goes to that worker."

Topic Exchange

Publisher → [exchange:topic] ──"order.*"────► all-orders-queue
                             └"order.high"──► high-priority-queue
                             └"#.error"────► error-monitor-queue
                             └"payment.eu.*"─► eu-payments-queue

Routing keys are dot-delimited; * matches one word, # matches zero or more. The most common choice for real systems.

Fanout Exchange

Publisher → [exchange:fanout] ─► queue 1 → consumer A
                              └► queue 2 → consumer B
                              └► queue 3 → consumer C

Routing key ignored — every bound queue gets a copy. Use when "everybody who cares should hear this."

Headers Exchange

Publisher → [exchange:headers]  message headers must match the binding's header criteria

Powerful but rare. Reach for topic first.

Producer (Node.js)

const amqp = require('amqplib');

const conn = await amqp.connect('amqp://user:password@rabbitmq:5672/myvhost');
const channel = await conn.createConfirmChannel();        // publisher confirms

await channel.assertExchange('orders', 'topic', {
  durable: true,                                          // survives broker restart
});

channel.publish(
  'orders',
  'order.created.eu',                                     // routing key
  Buffer.from(JSON.stringify(order)),
  {
    persistent: true,                                     // survive broker restart
    messageId: order.id,
    contentType: 'application/json',
    headers: {
      'x-source': 'web',
      'x-trace-id': req.traceId,
    },
  },
);

await channel.waitForConfirms();                          // wait for broker confirm

Publisher confirms are RabbitMQ's "ack from the broker" — without them, publish is fire-and-forget. Always use them in production.

Consumer (Node.js)

const channel = await conn.createChannel();

await channel.assertQueue('order-processing', {
  durable: true,
  arguments: {
    'x-dead-letter-exchange': 'orders.dlx',               // dead-letter destination
    'x-dead-letter-routing-key': 'order.failed',
    'x-message-ttl': 300000,                              // 5 min, then DLX
  },
});

// Bind to the topic exchange — receive all order.created.* events
await channel.bindQueue('order-processing', 'orders', 'order.created.*');

// Prefetch — at most 10 unacked messages outstanding at a time
await channel.prefetch(10);

channel.consume('order-processing', async (msg) => {
  if (!msg) return;
  try {
    const order = JSON.parse(msg.content.toString());
    await processOrder(order);
    channel.ack(msg);
  } catch (err) {
    // Reject and don't requeue → goes to DLX
    channel.nack(msg, false, false);
  }
});

Prefetch Is Backpressure

prefetch(N) says "broker, give me up to N unacked messages, then stop." Without it, the broker shoves every queued message at the consumer immediately — one slow consumer hoards everything. Always set prefetch.

Sensible starting point: prefetch = N where N ≈ how many messages your worker can process concurrently.

Dead-Letter Queues

When a message is nack'd with requeue=false, or expires (x-message-ttl), or is rejected from a full queue (x-max-length with x-overflow=reject-publish-dlx), it gets routed to the queue's configured dead-letter exchange:

// Set up a DLX for inspection / later replay
await channel.assertExchange('orders.dlx', 'topic', { durable: true });
await channel.assertQueue('orders.dead', { durable: true });
await channel.bindQueue('orders.dead', 'orders.dlx', '#');

// And the main queue with TTL and DLX configured (above)

Don't use the main queue as a graveyard. Inspect the DLQ on a schedule, fix the upstream issue, replay from there.

Reliability Recipes

Quorum Queues (default in 3.13+)

The old "classic" queues used mirroring. Use quorum queues for everything new — Raft-based, designed for durability across a cluster:

await channel.assertQueue('orders', {
  durable: true,
  arguments: { 'x-queue-type': 'quorum' },
});

Lazy Queues (large backlogs)

Default queues keep messages in memory until they hit a threshold. Lazy queues keep them on disk by default — much better when queues might grow large:

arguments: { 'x-queue-mode': 'lazy' }

Streams (the Kafka-like option)

RabbitMQ Streams are a newer queue type — append-only, with offset-based reading and large message replay. Use when you want Kafka semantics but already operate RabbitMQ.

Patterns

Work Queues (load-balance tasks)

Publisher → [default exchange] ──"task-queue"──► task-queue ──► worker 1
                                                              ├► worker 2
                                                              └► worker 3

Direct exchange (the default one), one queue, many consumers. Each message goes to one consumer.

Pub/Sub (everyone gets a copy)

Publisher → [fanout exchange] ─► queue per consumer ─► consumer

Each consumer has its own queue bound to the fanout — they all see every message.

Topic-based Routing

Publisher → [topic exchange] ─"region.us"──► us-queue   → US handlers
                             ─"region.eu"──► eu-queue   → EU handlers
                             ─"#.audit"────► audit-queue → audit log

Same exchange, multiple consumers, each interested in a slice of traffic.

Request / Reply (RPC)

Client → [default exchange] ─"rpc.queue"──► rpc-queue → server
Server → [default exchange] ─"reply.uuid"─► reply-queue → client (correlation_id)

AMQP has built-in reply_to and correlation_id fields for this — useful for short, synchronous-feeling calls over an async bus.

Clustering

RabbitMQ scales by clustering nodes:

  • Connections spread across nodes (load balancer in front).
  • Queues live primarily on one node (the leader); replicas (followers) on others if it's a quorum queue.
  • Network partitions are real; configure cluster_partition_handling carefully (autoheal or pause_minority).

For most teams: 3 nodes, quorum queues, an HAProxy or cloud LB in front. Beyond that you start trading complexity for scale; consider whether you actually want Kafka.

Operational Notes

Management UI

http://localhost:15672 (when you run the :management image) — exchanges, queues, bindings, consumers, message rates, all visible. Use it.

CLI

rabbitmqctl list_queues name messages consumers messages_unacknowledged
rabbitmqctl list_exchanges
rabbitmqctl list_bindings
rabbitmqctl list_connections
rabbitmqctl list_consumers
rabbitmqctl close_connection "<conn>" "going away"

What to Monitor

MetricThreshold to think about
messages_ready per queueGrowing = consumers can't keep up
messages_unacknowledgedHung consumers (delivered but not acked)
disk_free_alarm / mem_alarmBroker stops accepting publishes — page immediately
Channel churnMany short-lived channels suggests a bug; reuse them
Connection churnSame — connections are expensive; reuse

Performance Knobs

KnobEffect
prefetchPer-consumer concurrency cap (the most impactful)
Persistent vs transient messagesPersistent forces a disk write — slower, but survives restart
Lazy queuesLess memory, more disk
Quorum queuesSlightly slower than classic; much more durable
Connection / channel poolingReuse aggressively in clients

Best Practices Checklist

Production-ready RabbitMQ checklist

  • Cluster of ≥ 3 nodes with quorum queues
  • Load balancer or DNS-based LB in front of the cluster
  • Connections and channels reused (one connection per process, channels per thread)
  • Publisher confirms on every publisher
  • prefetch set on every consumer
  • persistent: true on messages you can't afford to lose
  • Dead-letter exchange on every important queue
  • Queue TTL (x-message-ttl) or length limit (x-max-length) to bound memory
  • Vhosts to isolate apps / environments; per-vhost users with least-privilege permissions
  • TLS for client connections; mTLS for cluster traffic
  • cluster_partition_handling = pause_minority (or autoheal)
  • Monitoring on messages_ready, unacknowledged, disk/memory alarms, connection/channel counts
  • Backups of definitions.json (exchanges, queues, bindings, users) — re-applyable on rebuild

On this page