Skip to content

Bull Queue for Node.js — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Bull Queue for Node.js. We cover key concepts, practical examples, and best practices to help you master this topic.

Bull is a Redis-backed job queue for Node.js that provides job scheduling, priorities, retries, concurrency control, and real-time event monitoring.

What You Learn

You will learn how to set up Bull queues, define job processors, configure concurrency and retries, use job events, and build production-ready job pipelines.

Why It Matters

Bull is the most popular Redis queue for Node.js. It handles job persistence, rate limiting, delayed jobs, and complex workflows. Understanding Bull enables you to build reliable background processing in JavaScript applications.

Real-World Use

Doda Browser's notification service uses Bull for all Background Jobs. When a scan completes, Bull delivers Webhooks, sends emails, and updates statuses with guaranteed at-least-once delivery.

Basic Queue Setup

const Queue = require('bull');

const emailQueue = new Queue('email', 'redis://127.0.0.1:6379');

// Producer
emailQueue.add({
  to: 'user@example.com',
  subject: 'Welcome',
  body: 'Thank you for joining!'
});

// Consumer
emailQueue.process(async (job) => {
  console.log(`Sending email to ${job.data.to}`);
  console.log(`Subject: ${job.data.subject}`);
  await simulateEmailSend();
  console.log(`Email sent to ${job.data.to}`);
});

function simulateEmailSend() {
  return new Promise(resolve => setTimeout(resolve, 1000));
}

Producer and Consumer

const Queue = require('bull');

const imageQueue = new Queue('image-processing', 'redis://127.0.0.1:6379');

// Producer
async function queueImageProcessing(imagePath) {
  const job = await imageQueue.add(
    { path: imagePath, operations: ['resize', 'thumbnail'] },
    {
      attempts: 3,
      backoff: { type: 'exponential', delay: 2000 },
      removeOnComplete: true,
      removeOnFail: false,
    }
  );
  console.log(`Image job created: ${job.id}`);
  return job.id;
}

// Consumer
imageQueue.process(async (job) => {
  console.log(`Processing image: ${job.data.path}`);
  for (const op of job.data.operations) {
    console.log(`  Running ${op}...`);
    await new Promise(r => setTimeout(r, 500));
    console.log(`  ${op} complete`);
  }
  return { status: 'done', path: job.data.path };
});

queueImageProcessing('/uploads/photo.jpg');

Concurrency Control

const Queue = require('bull');

const reportQueue = new Queue('reports', 'redis://127.0.0.1:6379');

// Process 3 jobs at a time
reportQueue.process(3, async (job) => {
  console.log(`Generating report: ${job.data.name}`);
  await new Promise(r => setTimeout(r, 2000));
  console.log(`Report generated: ${job.data.name}`);
  return { url: `/reports/${job.data.name}.pdf` };
});

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

reportQueue.on('failed', (job, err) => {
  console.log(`Job ${job.id} failed: ${err.message}`);
});

// Add multiple reports
for (let i = 0; i < 5; i++) {
  reportQueue.add({ name: `report_${i}` });
}

Expected output:

Generating report: report_0
Generating report: report_1
Generating report: report_2
Report generated: report_0
Job 1 completed: /reports/report_0.pdf
Report generated: report_1
Job 2 completed: /reports/report_1.pdf
...

Job Events

const Queue = require('bull');
const EventEmitter = require('events');

const webhookQueue = new Queue('webhooks', 'redis://127.0.0.1:6379');

webhookQueue.on('waiting', (jobId) => {
  console.log(`Job ${jobId} is waiting`);
});

webhookQueue.on('active', (job) => {
  console.log(`Job ${job.id} started processing`);
});

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

webhookQueue.on('failed', (job, err) => {
  console.log(`Job ${job.id} failed: ${err.message}`);
});

webhookQueue.on('progress', (job, progress) => {
  console.log(`Job ${job.id} progress: ${progress}%`);
});

webhookQueue.on('stalled', (job) => {
  console.log(`Job ${job.id} stalled, will be retried`);
});

webhookQueue.on('error', (error) => {
  console.error(`Queue error: ${error.message}`);
});

webhookQueue.process(async (job) => {
  let progress = 0;
  const interval = setInterval(() => {
    progress += 25;
    job.progress(progress);
    if (progress >= 100) clearInterval(interval);
  }, 200);

  await new Promise(r => setTimeout(r, 800));
  return { status: 'delivered' };
});

webhookQueue.add({ url: 'https://example.com/hook' });

Scheduling and Delayed Jobs

const Queue = require('bull');

const schedulerQueue = new Queue('scheduler', 'redis://127.0.0.1:6379');

// Delayed job (run after 10 seconds)
schedulerQueue.add(
  { task: 'cleanup' },
  { delay: 10000 }
);

// Job at specific timestamp
const timestamp = Date.now() + 60000; // 1 minute from now
schedulerQueue.add(
  { task: 'backup' },
  { timestamp }
);

// Repeating job (every 5 seconds)
schedulerQueue.add(
  { task: 'heartbeat' },
  { repeat: { every: 5000 } }
);

// Repeating job with cron (every hour)
schedulerQueue.add(
  { task: 'hourly_report' },
  { repeat: { cron: '0 * * * *' } }
);

schedulerQueue.process(async (job) => {
  console.log(`Executing: ${job.data.task} at ${new Date().toISOString()}`);
});

Job Prioritization

const Queue = require('bull');

const priorityQueue = new Queue('priorities', 'redis://127.0.0.1:6379');

// Add jobs with different priorities (lower number = higher priority)
priorityQueue.add({ task: 'critical_alert' }, { priority: 1 });
priorityQueue.add({ task: 'user_action' }, { priority: 5 });
priorityQueue.add({ task: 'cleanup' }, { priority: 10 });

priorityQueue.process(1, async (job) => {
  console.log(`Processing: ${job.data.task} (priority ${job.opts.priority})`);
  await new Promise(r => setTimeout(r, 300));
  return 'done';
});

setTimeout(() => {
  // High-priority jobs are processed before lower-priority ones
  console.log('Jobs processed in priority order');
}, 2000);

Common Mistakes

1. Not Handling Job Failures

Without error handling in the processor, thrown exceptions crash the Process. Always wrap processor logic in try/catch. Bull retries failed jobs automatically.

2. Forgetting to Set Redis URL

Bull defaults to redis://127.0.0.1:6379. In production, use environment variables for the Redis URL. Never hardcode connection strings.

3. Using Default Concurrency

Bull default concurrency is 1 (serial processing). For parallel processing, explicitly set concurrency in queue.process(concurrency, handler).

4. Not Removing Completed Jobs

Completed jobs accumulate in Redis memory. Set removeOnComplete: true and removeOnFail: false to manage memory.

5. Ignoring Job Stalled Detection

Bull marks jobs as stalled if they are processing without progress for too long. Set stalledInterval and provide progress updates for long-running jobs.

Practice Questions

1. How do you create a Bull queue?

new Queue('name', 'redis://host:6379'). The name identifies the queue. The Redis URL specifies the backend.

2. How does Bull handle job retries?

Set attempts and backoff in job options. Bull retries failed jobs automatically with configurable backoff (fixed or exponential).

3. How do you control job concurrency?

Pass a number to queue.process(concurrency, handler). Bull runs up to that many jobs in parallel.

4. What events does Bull emit?

waiting, active, completed, failed, progress, stalled, error, cleaned, drained, paused, resumed.

Challenge

Build a Bull-based video processing pipeline: upload, validate format (priority 5), transcode to 3 resolutions (priority 3), generate thumbnails (priority 5), extract metadata (priority 7), notify user (priority 2). Implement concurrency per step and progress tracking.

FAQ

What version of Node.js does Bull support?

Bull 4.x requires Node.js 12 or later. Bull 3.x supports Node.js 8+.

Can Bull use multiple Redis instances?

No, one Bull queue uses one Redis instance. For multi-region setups, use separate Bull instances per region.

How does Bull handle Redis connection failures?

Bull automatically reconnects to Redis. In-flight jobs during disconnection are marked as stalled and retried.

Can Bull queues be clustered?

Yes. Multiple Node.js processes can process the same Bull queue. Jobs are distributed automatically via Redis.

What is the maximum job size in Bull?

Limited by Redis (default 512MB). For larger payloads, store data externally and pass references.

Mini Project: Bull Job Pipeline

const Queue = require('bull');

const pipelineQueue = new Queue('pipeline', 'redis://127.0.0.1:6379');

pipelineQueue.process(async (job) => {
  const { steps } = job.data;
  const results = [];

  for (let i = 0; i < steps.length; i++) {
    const step = steps[i];
    job.progress(Math.round(((i + 1) / steps.length) * 100));

    console.log(`Step ${i + 1}/${steps.length}: ${step.name}`);
    await new Promise(r => setTimeout(r, 500));
    results.push({ step: step.name, status: 'done' });
  }

  return results;
});

pipelineQueue.on('completed', (job, result) => {
  console.log('Pipeline complete:', result.length, 'steps');
});

pipelineQueue.on('failed', (job, err) => {
  console.error('Pipeline failed:', err.message);
});

pipelineQueue.add({
  steps: [
    { name: 'validate' },
    { name: 'transcode' },
    { name: 'thumbnail' },
    { name: 'notify' },
  ]
});

Expected output:

Step 1/4: validate
Step 2/4: transcode
Step 3/4: thumbnail
Step 4/4: notify
Pipeline complete: 4 steps

What's Next

Now that you understand Bull, explore Sidekiq for Ruby for background jobs in Ruby applications, then learn about Huey for Python as a lightweight alternative.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro