| name | queue-job-processor |
| description | Implements background job processing with BullMQ/Redis including job queues, workers, scheduling, retries, and monitoring. Use when users request "background jobs", "queue processing", "async tasks", "BullMQ", or "job scheduler". |
Queue Job Processor
Build robust background job processing with BullMQ and Redis.
Core Workflow
- Setup Redis: Configure connection
- Create queues: Define job queues
- Implement workers: Process jobs
- Add job types: Type-safe job definitions
- Configure retries: Handle failures
- Add monitoring: Dashboard and alerts
Installation
npm install bullmq ioredis
npm install -D @types/ioredis
Redis Connection
import IORedis from 'ioredis';
export const redis = new IORedis(process.env.REDIS_URL!, {
maxRetriesPerRequest: null,
enableReadyCheck: false,
});
export const redisSubscriber = new IORedis(process.env.REDIS_URL!, {
maxRetriesPerRequest: null,
enableReadyCheck: false,
});
Queue Setup
Define Job Types
export interface EmailJobData {
to: string;
subject: string;
template: string;
variables: Record<string, string>;
}
export interface ImageProcessingJobData {
imageId: string;
userId: string;
operations: Array<{
type: 'resize' | 'crop' | 'watermark';
params: Record<string, any>;
}>;
}
export interface ReportJobData {
reportId: string;
userId: string;
type: 'daily' | 'weekly' | 'monthly';
dateRange: {
start: string;
end: string;
};
}
export interface WebhookJobData {
url: string;
payload: Record<, >;
?: <, >;
?: ;
}
=
| { : ; : }
| { : ; : }
| { : ; : }
| { : ; : };
Create Queues
import { Queue, QueueOptions } from 'bullmq';
import { redis } from '../lib/redis';
import {
EmailJobData,
ImageProcessingJobData,
ReportJobData,
WebhookJobData,
} from './types';
const defaultOptions: QueueOptions = {
connection: redis,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
},
removeOnComplete: {
count: 1000,
age: 24 * 3600,
},
removeOnFail: {
count: 5000,
},
},
};
export const emailQueue = new Queue<EmailJobData>('email', defaultOptions);
export const imageQueue = new Queue<ImageProcessingJobData>(, {
...defaultOptions,
: {
...defaultOptions.,
: ,
: * * ,
},
});
reportQueue = <>(, {
...defaultOptions,
: {
...defaultOptions.,
: * * ,
},
});
webhookQueue = <>(, {
...defaultOptions,
: {
...defaultOptions.,
: ,
: {
: ,
: ,
},
},
});
Workers
Email Worker
import { Worker, Job } from 'bullmq';
import { redis } from '../lib/redis';
import { EmailJobData } from '../jobs/types';
import { sendEmail } from '../lib/email';
const emailWorker = new Worker<EmailJobData>(
'email',
async (job: Job<EmailJobData>) => {
const { to, subject, template, variables } = job.data;
console.log(`Processing email job ${job.id} to ${to}`);
await job.updateProgress(10);
const html = await renderTemplate(template, variables);
await job.updateProgress(50);
const result = await sendEmail({
to,
subject,
html,
});
await job.updateProgress();
{ : result., : () };
},
{
: redis,
: ,
: {
: ,
: ,
},
}
);
emailWorker.(, {
.(, result);
});
emailWorker.(, {
.(, error);
});
emailWorker.(, {
.();
});
{ emailWorker };
Image Processing Worker
import { Worker, Job } from 'bullmq';
import { redis } from '../lib/redis';
import { ImageProcessingJobData } from '../jobs/types';
import sharp from 'sharp';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: process.env.AWS_REGION });
const imageWorker = new Worker<ImageProcessingJobData>(
'image-processing',
async (job: Job<ImageProcessingJobData>) => {
const { imageId, userId, operations } = job.data;
console.log(`Processing image ${imageId} for user ${userId}`);
const originalBuffer = await downloadImage(imageId);
let image = sharp(originalBuffer);
for ( i = ; i < operations.; i++) {
op = operations[i];
(op.) {
:
image = image.(op.., op.., {
: op.. || ,
});
;
:
image = image.({
: op..,
: op..,
: op..,
: op..,
});
;
:
image = image.([
{ : op.., : },
]);
;
}
job.(((i + ) / operations.) * );
}
processedBuffer = image.({ : }).();
key = ;
s3.(
({
: process..,
: key,
: processedBuffer,
: ,
})
);
job.();
{
: ,
: processedBuffer.,
};
},
{
: redis,
: ,
}
);
imageWorker.(, (job, error) => {
(job) {
(job.., {
: ,
: job..,
: error.,
});
}
});
{ imageWorker };
Webhook Worker with Retries
import { Worker, Job } from 'bullmq';
import { redis } from '../lib/redis';
import { WebhookJobData } from '../jobs/types';
const webhookWorker = new Worker<WebhookJobData>(
'webhooks',
async (job: Job<WebhookJobData>) => {
const { url, payload, headers = {} } = job.data;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': generateSignature(payload),
...headers,
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
if (response.status >= 500) {
throw ();
}
{
: ,
: response.,
: ,
};
}
{
: ,
: response.,
};
},
{
: redis,
: ,
}
);
{ webhookWorker };
Adding Jobs
Service Layer
import { emailQueue, imageQueue, reportQueue, webhookQueue } from '../queues';
import { JobsOptions } from 'bullmq';
export class JobService {
static async sendEmail(data: EmailJobData, options?: JobsOptions) {
return emailQueue.add('send-email', data, {
...options,
priority: data.template === 'password-reset' ? 1 : 10,
});
}
static async sendBulkEmails(emails: EmailJobData[]) {
const jobs = emails.map((data, index) => ({
name: 'send-email',
data,
opts: {
delay: index * 100,
},
}));
return emailQueue.addBulk(jobs);
}
static async processImage() {
imageQueue.(, data, {
: ,
});
}
() {
reportQueue.(, data, {
: runAt.() - .(),
});
}
() {
webhookQueue.(, data);
}
}
API Usage
import { JobService } from '@/services/jobs.service';
export async function POST(req: Request) {
const data = await req.json();
const user = await db.user.create({ data });
await JobService.sendEmail({
to: user.email,
subject: 'Welcome!',
template: 'welcome',
variables: { name: user.name },
});
return Response.json(user);
}
Scheduled Jobs (Cron)
import { Queue, QueueScheduler } from 'bullmq';
import { redis } from '../lib/redis';
export async function setupSchedulers() {
await reportQueue.add(
'cleanup',
{},
{
repeat: {
pattern: '0 0 * * *',
},
}
);
await metricsQueue.add(
'aggregate',
{},
{
repeat: {
pattern: '0 * * * *',
},
}
);
await emailQueue.add(
'weekly-digest',
{ template: 'weekly-digest' },
{
repeat: {
pattern: '0 9 * * 1',
},
}
);
}
Job Events & Monitoring
Event Listeners
import { QueueEvents } from 'bullmq';
import { redis } from '../lib/redis';
const emailQueueEvents = new QueueEvents('email', { connection: redis });
emailQueueEvents.on('completed', ({ jobId, returnvalue }) => {
console.log(`Job ${jobId} completed with:`, returnvalue);
metrics.increment('email.completed');
});
emailQueueEvents.on('failed', ({ jobId, failedReason }) => {
console.error(`Job ${jobId} failed:`, failedReason);
metrics.increment('email.failed');
alertOnFailure(jobId, failedReason);
});
emailQueueEvents.on('delayed', ({ jobId, delay }) => {
console.log(`Job ${jobId} delayed by ${delay}ms`);
});
emailQueueEvents.on('progress', ({ jobId, data }) => {
console.(, data);
});
emailQueueEvents.(, {
.();
metrics.();
});
Bull Board Dashboard
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
import { emailQueue, imageQueue, reportQueue, webhookQueue } from '@/queues';
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath('/api/admin/queues');
createBullBoard({
queues: [
new BullMQAdapter(emailQueue),
new BullMQAdapter(imageQueue),
new BullMQAdapter(reportQueue),
new BullMQAdapter(webhookQueue),
],
serverAdapter,
});
export const GET = serverAdapter.getRouter();
export const POST = serverAdapter.getRouter();
Error Handling
import { Worker, Job, UnrecoverableError } from 'bullmq';
export class NonRetryableError extends UnrecoverableError {
constructor(message: string) {
super(message);
this.name = 'NonRetryableError';
}
}
const worker = new Worker(
'queue-name',
async (job: Job) => {
try {
if (!job.data.requiredField) {
throw new NonRetryableError('Missing required field');
}
return await processJob(job.data);
} catch (error) {
if (error instanceof NonRetryableError) {
throw error;
}
.(, error);
error;
}
},
{
: redis,
}
);
worker.(, {
.(, error);
});
Graceful Shutdown
import { emailWorker, imageWorker, reportWorker } from './workers';
const workers = [emailWorker, imageWorker, reportWorker];
async function gracefulShutdown() {
console.log('Shutting down workers...');
await Promise.all(
workers.map((worker) =>
worker.close().catch((err) => {
console.error('Error closing worker:', err);
})
)
);
await redis.quit();
await redisSubscriber.quit();
console.log('Workers shut down');
process.exit(0);
}
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
Best Practices
- Idempotent jobs: Jobs should be safe to retry
- Unique job IDs: Prevent duplicate processing
- Set timeouts: Prevent stuck jobs
- Use progress updates: For long-running jobs
- Handle failures gracefully: Alert and log
- Clean up old jobs: Remove completed/failed jobs
- Graceful shutdown: Wait for jobs to complete
- Monitor queues: Use Bull Board or similar
Output Checklist
Every queue implementation should include: