Bull Queue Complete Guide
이 글의 핵심
Bull is a Redis-based queue for Node.js that handles job processing, retries, priorities, and delayed jobs. It's battle-tested and used by thousands of production apps.
Introduction
Bull is a Redis-based queue for Node.js that handles distributed job processing. It’s perfect for tasks that are too slow or unreliable to run in API request handlers.
Why Use Queues?
Without queue (blocking):
app.post('/send-email', async (req, res) => {
await sendEmail(req.body); // Takes 2-3 seconds
res.json({ message: 'Email sent' });
});
// User waits 2-3 seconds for response
// If email fails, request fails
With queue (non-blocking):
app.post('/send-email', async (req, res) => {
await emailQueue.add(req.body); // <10ms
res.json({ message: 'Email queued' });
});
// User gets instant response
// Email processed in background
// Automatic retries on failure
1. Installation
npm install bull
Requires Redis:
# Docker
docker run -d -p 6379:6379 redis
# macOS
brew install redis
redis-server
# Ubuntu
sudo apt install redis-server
2. Basic Queue
const Queue = require('bull');
// Create queue
const emailQueue = new Queue('email', {
redis: {
host: '127.0.0.1',
port: 6379,
}
});
// Add job to queue
await emailQueue.add({
to: '[email protected]',
subject: 'Welcome!',
body: 'Thanks for signing up',
});
// Process jobs
emailQueue.process(async (job) => {
console.log('Processing job:', job.id);
await sendEmail(job.data);
console.log('Job completed:', job.id);
});
3. Job Options
await emailQueue.add({
to: '[email protected]',
subject: 'Hello',
}, {
// Job options
attempts: 3, // Retry up to 3 times
backoff: {
type: 'exponential', // Exponential backoff
delay: 2000, // Start with 2 second delay
},
delay: 5000, // Delay job by 5 seconds
priority: 1, // Priority (1 = highest)
timeout: 30000, // Timeout after 30 seconds
removeOnComplete: true, // Remove from Redis when done
removeOnFail: false, // Keep failed jobs for debugging
});
4. Priority Queues
// Add jobs with different priorities
await queue.add({ task: 'critical' }, { priority: 1 }); // Highest
await queue.add({ task: 'normal' }, { priority: 5 });
await queue.add({ task: 'low' }, { priority: 10 }); // Lowest
// High priority jobs processed first
5. Delayed Jobs
// Process job after 1 hour
await queue.add({ task: 'reminder' }, {
delay: 60 * 60 * 1000, // 1 hour in milliseconds
});
// Process at specific time
const scheduledTime = new Date('2026-12-25T00:00:00Z');
await queue.add({ task: 'holiday-email' }, {
delay: scheduledTime.getTime() - Date.now(),
});
6. Repeatable Jobs (Cron)
// Run every 5 minutes
await queue.add({ task: 'cleanup' }, {
repeat: {
every: 5 * 60 * 1000, // 5 minutes
}
});
// Cron syntax
await queue.add({ task: 'daily-report' }, {
repeat: {
cron: '0 9 * * *', // Every day at 9 AM
}
});
// With timezone
await queue.add({ task: 'morning-email' }, {
repeat: {
cron: '0 8 * * *',
tz: 'America/New_York',
}
});
7. Job Progress
// Report progress
emailQueue.process(async (job) => {
await job.progress(0);
const emails = job.data.emails;
for (let i = 0; i < emails.length; i++) {
await sendEmail(emails[i]);
await job.progress(Math.round(((i + 1) / emails.length) * 100));
}
return { sent: emails.length };
});
// Listen for progress
queue.on('progress', (job, progress) => {
console.log(`Job ${job.id} is ${progress}% done`);
});
8. Job Events
// Job completed
queue.on('completed', (job, result) => {
console.log(`Job ${job.id} completed with result:`, result);
});
// Job failed
queue.on('failed', (job, err) => {
console.error(`Job ${job.id} failed:`, err.message);
});
// Job stalled (taking too long)
queue.on('stalled', (job) => {
console.warn(`Job ${job.id} stalled`);
});
// Job removed
queue.on('removed', (job) => {
console.log(`Job ${job.id} removed`);
});
// Global completed (all jobs)
queue.on('global:completed', (jobId, result) => {
console.log(`Job ${jobId} completed globally`);
});
9. Multiple Processors
// Processor 1: High priority
queue.process('high-priority', 5, async (job) => {
// Process up to 5 high-priority jobs concurrently
await processHighPriority(job.data);
});
// Processor 2: Low priority
queue.process('low-priority', 2, async (job) => {
// Process up to 2 low-priority jobs concurrently
await processLowPriority(job.data);
});
// Add jobs
await queue.add('high-priority', { task: 'urgent' });
await queue.add('low-priority', { task: 'background' });
10. Real-World Example: Email Service
const Queue = require('bull');
const nodemailer = require('nodemailer');
// Create queue
const emailQueue = new Queue('email', {
redis: process.env.REDIS_URL,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
removeOnComplete: 100, // Keep last 100 completed
removeOnFail: false, // Keep all failed for debugging
}
});
// Email transporter
const transporter = nodemailer.createTransport({ /* config */ });
// Process emails
emailQueue.process(async (job) => {
const { to, subject, html } = job.data;
console.log(`Sending email to ${to}`);
try {
await transporter.sendMail({ to, subject, html });
return { sent: true, to };
} catch (error) {
console.error(`Failed to send email to ${to}:`, error);
throw error; // Will retry
}
});
// Listen for events
emailQueue.on('completed', (job, result) => {
console.log(`Email sent to ${result.to}`);
});
emailQueue.on('failed', (job, err) => {
console.error(`Email to ${job.data.to} failed after ${job.attemptsMade} attempts`);
// Notify admin or log to monitoring service
});
// API endpoint
app.post('/api/send-email', async (req, res) => {
const { to, subject, html } = req.body;
const job = await emailQueue.add({
to,
subject,
html,
});
res.json({
message: 'Email queued',
jobId: job.id,
});
});
// Check job status
app.get('/api/jobs/:id', async (req, res) => {
const job = await emailQueue.getJob(req.params.id);
if (!job) {
return res.status(404).json({ error: 'Job not found' });
}
const state = await job.getState();
const progress = job.progress();
res.json({
id: job.id,
state,
progress,
data: job.data,
});
});
11. Rate Limiting
const queue = new Queue('api-calls', {
redis: process.env.REDIS_URL,
limiter: {
max: 100, // Max 100 jobs
duration: 60000, // Per 60 seconds
}
});
// Jobs are automatically rate-limited
await queue.add({ url: 'https://api.example.com/data' });
12. Bull Board (UI Dashboard)
npm install bull-board
const { createBullBoard } = require('@bull-board/api');
const { BullAdapter } = require('@bull-board/api/bullAdapter');
const { ExpressAdapter } = require('@bull-board/express');
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath('/admin/queues');
createBullBoard({
queues: [
new BullAdapter(emailQueue),
new BullAdapter(imageQueue),
new BullAdapter(reportQueue),
],
serverAdapter: serverAdapter,
});
app.use('/admin/queues', serverAdapter.getRouter());
// Visit http://localhost:3000/admin/queues
13. Job Cleanup
// Remove completed jobs older than 1 day
await queue.clean(24 * 3600 * 1000, 'completed');
// Remove failed jobs older than 1 week
await queue.clean(7 * 24 * 3600 * 1000, 'failed');
// Remove all jobs
await queue.empty();
// Automatic cleanup
emailQueue.on('completed', async (job) => {
await job.remove();
});
14. Concurrency
// Process 5 jobs concurrently
queue.process(5, async (job) => {
return await processJob(job.data);
});
// Named processors with different concurrency
queue.process('email', 10, async (job) => {
// Up to 10 email jobs at once
});
queue.process('image', 3, async (job) => {
// Up to 3 image jobs at once (CPU intensive)
});
15. Graceful Shutdown
// Handle shutdown
async function shutdown() {
console.log('Shutting down gracefully...');
// Close queue (waits for active jobs)
await emailQueue.close();
process.exit(0);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
16. Best Practices
1. Use Named Jobs
// Good: named jobs
await queue.add('send-welcome-email', { userId: 123 });
await queue.add('generate-report', { reportId: 456 });
queue.process('send-welcome-email', sendWelcomeEmail);
queue.process('generate-report', generateReport);
2. Idempotent Jobs
// Ensure jobs can be retried safely
queue.process(async (job) => {
const { userId, emailType } = job.data;
// Check if already sent
const sent = await db.emailLogs.findOne({ userId, emailType });
if (sent) {
console.log('Email already sent, skipping');
return;
}
// Send email
await sendEmail(userId, emailType);
// Log
await db.emailLogs.create({ userId, emailType, sentAt: new Date() });
});
3. Monitor Queue Health
setInterval(async () => {
const waiting = await queue.getWaitingCount();
const active = await queue.getActiveCount();
const failed = await queue.getFailedCount();
console.log(`Queue status - Waiting: ${waiting}, Active: ${active}, Failed: ${failed}`);
// Alert if too many jobs waiting
if (waiting > 1000) {
console.warn('Queue backlog too high!');
// Send alert
}
}, 60000); // Every minute
Summary
Bull provides reliable background job processing:
- Redis-based for distributed systems
- Automatic retries with backoff
- Job priorities and delays
- Cron-like scheduling for recurring jobs
- Progress tracking and events
Key Takeaways:
- Use for slow or unreliable tasks
- Configure retries and backoff
- Monitor queue health
- Use Bull Board for UI
- Graceful shutdown handling
Next Steps:
- Deploy with [Redis](/en/blog/redis-complete-guide/
- Scale with Docker
- Monitor with [Winston](/en/blog/winston-complete-guide/
Resources:
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Complete Bull guide for background jobs in Node.js. Learn job processing, retries, priorities, scheduling, and building … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++ Tag Dispatch | Compile-Time Algorithm Selection](/en/blog/cpp-tag-dispatch/
- [[2026] Go context: timeouts and cancellation](/en/blog/go-context-cancellation-guide/
- [Jest Testing Guide | Unit Tests· Mocks](/en/blog/jest-testing-guide/
이 글에서 다루는 키워드 (관련 검색어)
Bull, Queue, Redis, Node.js, Background Jobs, Async Processing 등으로 검색하시면 이 글이 도움이 됩니다.