RabbitMQ with Node.js
RabbitMQ is a battle-tested open-source message broker that implements the AMQP 0-9-1 protocol. It sits between producers and consumers, routing messages through a system of exchanges and queues. Unlike simpler in-process event emitters, RabbitMQ is a durable external process: messages survive application restarts, multiple services can consume from the same queue, and the broker tracks acknowledgements. This page covers the core model — exchanges, queues, bindings, and routing keys — then works through a complete Node.js example using the amqplib client.
Core model: exchanges, queues, bindings
Producer RabbitMQ broker Consumer(s)
│ │
│ publish(exchange, ┌─────────────┐ ┌──────────────┐ │
│ routingKey, msg) │ Exchange │ │ Queue │ │
└────────────────────────►│ ├───►│ (durable) ├───► worker-1
│ (routing │ │ │
│ logic) │ └──────────────┘ ► worker-2
└─────────────┘
│ │
Binding: routingKey matches?
Yes → message goes to queue
No → message dropped (or returned)Exchange types
Exchange type | Routing logic | Use when |
|---|---|---|
direct | Route to queue whose binding key exactly matches routing key | Work queues, task distribution |
fanout | Broadcast to ALL bound queues (routing key ignored) | Notifications to multiple services |
topic |
| Event routing by category (e.g. |
headers | Match on message header attributes instead of routing key | Complex routing conditions (rarely used) |
(default / nameless) | Routes to queue whose name equals the routing key | Simplest case — direct to a named queue |
Installing amqplib
npm install amqplib npm install --save-dev @types/amqplib
Producer — publishing a message
import amqp from 'amqplib'
async function publishJob(payload: object) {
const connection = await amqp.connect(process.env.RABBITMQ_URL ?? 'amqp://localhost')
const channel = await connection.createChannel()
const queue = 'jobs'
// assertQueue is idempotent: creates the queue if it doesn't exist
await channel.assertQueue(queue, { durable: true }) // survives broker restart
const message = Buffer.from(JSON.stringify(payload))
// persistent: 2 → message is written to disk, not just memory
channel.sendToQueue(queue, message, { persistent: true })
console.log('Published:', payload)
// Give the channel time to flush before closing
await channel.close()
await connection.close()
}Consumer — processing messages
import amqp from 'amqplib'
async function startWorker() {
const connection = await amqp.connect(process.env.RABBITMQ_URL ?? 'amqp://localhost')
const channel = await connection.createChannel()
const queue = 'jobs'
await channel.assertQueue(queue, { durable: true })
// prefetch(1): don't give this worker a second message until it acks the first.
// Without this, RabbitMQ round-robins messages regardless of how busy each worker is.
channel.prefetch(1)
console.log('Worker waiting for messages...')
channel.consume(queue, async (msg) => {
if (!msg) return // consumer cancelled by broker
try {
const payload = JSON.parse(msg.content.toString())
await processJob(payload) // your business logic
channel.ack(msg) // ✅ success — remove from queue
} catch (err) {
console.error('Job failed:', err)
// requeue: false → send to dead-letter queue instead of infinite retry loop
channel.nack(msg, false, false)
}
})
}
async function processJob(payload: unknown) {
// ... do the actual work
console.log('Processing:', payload)
}Persistent connection with reconnection
// In production, don't open/close per message — maintain one long-lived connection.
// amqplib doesn't auto-reconnect; handle connection errors explicitly:
import amqp, { Connection, Channel } from 'amqplib'
class RabbitMQClient {
private connection: Connection | null = null
private channel: Channel | null = null
async connect(): Promise<void> {
this.connection = await amqp.connect(process.env.RABBITMQ_URL!)
this.channel = await this.connection.createChannel()
this.connection.on('error', (err) => {
console.error('RabbitMQ connection error:', err)
this.reconnect()
})
this.connection.on('close', () => {
console.warn('RabbitMQ connection closed — reconnecting')
this.reconnect()
})
}
private reconnect(): void {
setTimeout(() => this.connect(), 5000) // simple backoff; use exponential in prod
}
getChannel(): Channel {
if (!this.channel) throw new Error('RabbitMQ not connected')
return this.channel
}
}
export const rabbit = new RabbitMQClient()Fanout exchange — broadcasting events
// Producer: publish to a fanout exchange — all bound queues receive a copy
const exchange = 'user.events'
await channel.assertExchange(exchange, 'fanout', { durable: true })
channel.publish(exchange, '', Buffer.from(JSON.stringify({ type: 'user.created', userId: '123' })))
// routing key '' is ignored for fanout
// Consumer: create an exclusive, auto-delete queue and bind it
const q = await channel.assertQueue('', { exclusive: true }) // server-generated name
await channel.bindQueue(q.queue, exchange, '')
channel.consume(q.queue, (msg) => {
if (!msg) return
const event = JSON.parse(msg.content.toString())
// handle event...
channel.ack(msg)
})Key options summary
Option | Where set | What it does |
|---|---|---|
| assertQueue / assertExchange | Survives broker restart |
| sendToQueue / publish | Message written to disk (needs durable queue too) |
| channel | Max unacked messages per worker — prevents overloading slow workers |
| assertQueue | Only this connection can use the queue; auto-deleted on disconnect |
| assertQueue | Queue deleted when last consumer disconnects |
| assertQueue args | Where nacked / expired messages go |