March 25, 2026·8 min·By

Message Queues in Node.js: BullMQ, RabbitMQ, and When to Use Each

message queueBullMQRabbitMQNode.jsbackground jobs

If your API is doing work that takes more than 100ms -- sending emails, processing images, syncing data -- you should be offloading it to a queue. Here's how.

When You Need a Queue

  • Email sending (SMTP calls are slow and can fail)
  • Image/video processing
  • PDF generation
  • External API calls that can fail and need retry
  • Webhook delivery
  • Large batch operations
  • Anything that doesn't need to be done synchronously

The pattern: API receives request, puts a job in the queue, returns 202 Accepted. Worker picks up job, processes it, reports result.

BullMQ (Redis-based, Recommended for Node.js)

BullMQ is the best choice for most Node.js applications:

npm install bullmq ioredis
// queue.ts
import { Queue, Worker, QueueEvents } from 'bullmq'
import IORedis from 'ioredis'

const connection = new IORedis(process.env.REDIS_URL!)

// Create a queue
export const emailQueue = new Queue('emails', { connection })

// Add a job
export async function queueWelcomeEmail(userId: string) {
    await emailQueue.add(
        'welcome-email',
        { userId },
        {
            attempts: 3,              // Retry up to 3 times
            backoff: {
                type: 'exponential',
                delay: 2000           // 2s, 4s, 8s between retries
            },
            removeOnComplete: 100,   // Keep last 100 completed jobs
            removeOnFail: 200,       // Keep last 200 failed jobs
        }
    )
}
// worker.ts
const emailWorker = new Worker(
    'emails',
    async (job) => {
        if (job.name === 'welcome-email') {
            const { userId } = job.data
            const user = await db.users.findById(userId)
            await sendEmail({
                to: user.email,
                template: 'welcome',
                data: { name: user.name }
            })
            return { sent: true }
        }
    },
    {
        connection,
        concurrency: 5,  // Process 5 jobs simultaneously
    }
)

emailWorker.on('completed', (job, result) => {
    console.log(`Job ${job.id} completed:`, result)
})

emailWorker.on('failed', (job, error) => {
    console.error(`Job ${job?.id} failed:`, error.message)
    // Alert if all retries exhausted
    if (job?.attemptsMade === job?.opts.attempts) {
        alertOncall(`Email job ${job.id} failed permanently`)
    }
})

Job Prioritization

// Higher priority jobs are processed first (lower number = higher priority)
await emailQueue.add('password-reset', data, { priority: 1 })    // Urgent
await emailQueue.add('marketing-email', data, { priority: 10 })  // Can wait

Scheduled Jobs

// Recurring job
await emailQueue.add(
    'weekly-digest',
    {},
    {
        repeat: { cron: '0 9 * * 1' }  // Every Monday at 9am
    }
)

// Delayed job
await emailQueue.add(
    'follow-up-email',
    { userId },
    {
        delay: 3 * 24 * 60 * 60 * 1000  // 3 days from now
    }
)

RabbitMQ: When You Need Cross-Language Pub/Sub

If you have services in multiple languages or need complex routing:

import amqp from 'amqplib'

const connection = await amqp.connect(process.env.RABBITMQ_URL!)
const channel = await connection.createChannel()

// Declare exchange and queues
await channel.assertExchange('events', 'topic', { durable: true })
await channel.assertQueue('order-processing', { durable: true })
await channel.bindQueue('order-processing', 'events', 'order.*')

// Publish
channel.publish(
    'events',
    'order.created',
    Buffer.from(JSON.stringify({ orderId: '123', amount: 5000 })),
    { persistent: true }  // Survive broker restart
)

// Consume
channel.consume('order-processing', (msg) => {
    if (!msg) return
    const order = JSON.parse(msg.content.toString())
    processOrder(order)
        .then(() => channel.ack(msg))
        .catch(() => channel.nack(msg, false, true))  // Requeue on failure
}, { noAck: false })

BullMQ vs RabbitMQ

BullMQ RabbitMQ
Language Node.js native Any language
Persistence Redis RabbitMQ broker
Job UI Bull Board RabbitMQ management
Delayed jobs Built-in Plugin needed
Priority Built-in Per-queue
Complexity Low Medium
Best for Node.js background jobs Multi-service event bus

Bull Board: Job Monitoring UI

import { createBullBoard } from '@bull-board/api'
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'
import { ExpressAdapter } from '@bull-board/express'

const serverAdapter = new ExpressAdapter()
createBullBoard({
    queues: [new BullMQAdapter(emailQueue)],
    serverAdapter
})

app.use('/admin/queues', serverAdapter.getRouter())

At localhost:3000/admin/queues, you'll see all jobs, their status, retry counts, and the ability to retry failed jobs manually.

Start with BullMQ for any Node.js app that needs background processing. Only add RabbitMQ when you need cross-service event routing that BullMQ can't handle.

K
Founder & Technical Lead, Innovibe

Building software for 15+ years. Passionate about AI, system design, and shipping things that work.

Frequently asked questions

How do I handle database migrations safely in production?+

Always run migrations before deploying new code (never after). Use additive-only migrations — add columns with defaults, never rename or drop in the same release. Blue-green deployments make this much safer.

When should I add a Redis cache vs just querying Postgres?+

Add Redis when the same query runs more than ~10 times per second, when query time exceeds 10ms and latency matters, or when you need pub/sub. Don't add it preemptively — optimise the query first.

Does Innovibe build this kind of thing for clients?+

Yes — this is exactly what we do day-to-day for clients across BC and Canada. If you'd rather have us build and maintain it than implement it yourself, reach out.

How do I decide whether to build this in-house or hire an agency?+

Build in-house if your team has the skills and bandwidth and this is core to your product. Hire out if it's infrastructure, if speed matters, or if the expertise gap would take months to close. We're biased, obviously — but we'll tell you honestly when in-house makes more sense.

What tech stack does Innovibe use for projects like this?+

Next.js + TypeScript on the frontend, Node.js or Go on the backend, Postgres for the primary data store, and GCP (Cloud Run, BigQuery, Pub/Sub) for infrastructure. We pick tools that are boring in the best way — proven, well-documented, and easy to hire for.

Building something with AI?

We scope and ship AI features quickly. Let's talk.

Start a Conversation