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
| Term | What it is |
|---|---|
| Connection | TCP connection to the broker |
| Channel | Lightweight virtual connection inside a Connection; what you use |
| Exchange | Receives messages from publishers and routes them |
| Queue | Buffer that holds messages until consumed |
| Binding | The rule connecting an exchange to a queue |
| Routing key | A string attached to each message; exchanges use it |
| Vhost (virtual host) | Logical namespace; permissions and resources isolated |
| Consumer | Subscribes to a queue, gets messages pushed to it |
| Ack / Nack | Consumer confirms (or rejects) successful processing |
| DLX (dead-letter exchange) | Where messages go after they're rejected or expire |
Exchanges: Four Routing Strategies
| Type | Routes by | Use case |
|---|---|---|
| direct | Exact routing key match | Task queues, point-to-point |
| topic | Pattern match (order.*, #.error) | Selective event routing — most flexible |
| fanout | Broadcast to all bound queues | Notifications, cache invalidation, pub/sub |
| headers | Match on message header values | Rare; complex multi-dimensional routing |
Direct Exchange
Publisher → [exchange:direct] ──"images"──► images-queue → image worker
└"emails"──► emails-queue → email worker
└"reports"─► reports-queue → report workerEach 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-queueRouting 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 CRouting 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 criteriaPowerful 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 confirmPublisher 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 3Direct 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 ─► consumerEach 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 logSame 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_handlingcarefully (autohealorpause_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
| Metric | Threshold to think about |
|---|---|
messages_ready per queue | Growing = consumers can't keep up |
messages_unacknowledged | Hung consumers (delivered but not acked) |
disk_free_alarm / mem_alarm | Broker stops accepting publishes — page immediately |
| Channel churn | Many short-lived channels suggests a bug; reuse them |
| Connection churn | Same — connections are expensive; reuse |
Performance Knobs
| Knob | Effect |
|---|---|
prefetch | Per-consumer concurrency cap (the most impactful) |
| Persistent vs transient messages | Persistent forces a disk write — slower, but survives restart |
| Lazy queues | Less memory, more disk |
| Quorum queues | Slightly slower than classic; much more durable |
| Connection / channel pooling | Reuse 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
-
prefetchset on every consumer -
persistent: trueon 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(orautoheal) - Monitoring on
messages_ready,unacknowledged, disk/memory alarms, connection/channel counts - Backups of
definitions.json(exchanges, queues, bindings, users) — re-applyable on rebuild