# Background Jobs & Queues mit BullMQ **Meta-Description:** Background Job Processing mit BullMQ und Redis. Queues, Workers, Retries und Job Scheduling für Node.js. **Keywords:** BullMQ, Background Jobs, Queue Processing, Redis Queue, Job Scheduling, Worker Threads, Async Processing --- ## Einführung Nicht jede Aufgabe sollte im Request-Response-Zyklus laufen. **Background Jobs** ermöglichen Email-Versand, Image Processing, Reports – alles asynchron mit Retries, Prioritäten und Scheduling. --- ## Warum Background Jobs? ``` ┌─────────────────────────────────────────────────────────────┐ │ SYNCHRON vs ASYNCHRON │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Synchron (schlecht für): │ │ ├── Email-Versand (3-5s) │ │ ├── Image/Video Processing (10s+) │ │ ├── PDF Generation (2-5s) │ │ ├── Externe API Calls (variabel) │ │ └── Batch Operations (Minuten) │ │ │ │ User wartet... ⏳ │ │ │ │ Asynchron (Background): │ │ ├── Request → Response (50ms) │ │ ├── Job in Queue → Worker verarbeitet │ │ ├── Retries bei Fehlern │ │ ├── Rate Limiting │ │ └── Scheduling (Cron) │ │ │ │ User bekommt sofort Antwort! ✓ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## BullMQ Setup ```bash npm install bullmq ioredis ``` ```typescript // lib/queue.ts import { Queue, Worker, QueueEvents } from 'bullmq'; import IORedis from 'ioredis'; const connection = new IORedis(process.env.REDIS_URL!, { maxRetriesPerRequest: null }); // Queue definieren export const emailQueue = new Queue('email', { connection }); export const imageQueue = new Queue('image-processing', { connection }); export const reportQueue = new Queue('reports', { connection }); // Queue Events (für Monitoring) export const emailQueueEvents = new QueueEvents('email', { connection }); ``` --- ## Job Types definieren ```typescript // types/jobs.ts export interface EmailJob { type: 'welcome' | 'reset-password' | 'notification'; to: string; subject: string; template: string; data: Record; } export interface ImageProcessingJob { imageId: string; operations: Array<{ type: 'resize' | 'crop' | 'watermark'; params: Record; }>; outputFormat: 'webp' | 'avif' | 'jpeg'; } export interface ReportJob { reportType: 'daily' | 'weekly' | 'monthly'; userId: string; dateRange: { start: Date; end: Date; }; } ``` --- ## Jobs hinzufügen ```typescript // services/email.ts import { emailQueue } from '@/lib/queue'; import { EmailJob } from '@/types/jobs'; export async function sendWelcomeEmail(userId: string, email: string) { const job = await emailQueue.add('send-email', { type: 'welcome', to: email, subject: 'Welcome to Our Platform!', template: 'welcome', data: { userId } } satisfies EmailJob, { // Job Options attempts: 3, backoff: { type: 'exponential', delay: 1000 // 1s, 2s, 4s }, removeOnComplete: { count: 1000 // Behalte letzte 1000 completed Jobs }, removeOnFail: { count: 5000 // Behalte letzte 5000 failed Jobs } }); return job.id; } // Mit Priorität export async function sendUrgentEmail(data: EmailJob) { await emailQueue.add('send-email', data, { priority: 1 // Niedrigere Zahl = höhere Priorität }); } // Verzögerter Job export async function scheduleEmail(data: EmailJob, delay: number) { await emailQueue.add('send-email', data, { delay // Millisekunden }); } // Geplanter Job (Cron) export async function scheduleReports() { await reportQueue.add('generate-report', { reportType: 'daily', // ... }, { repeat: { pattern: '0 8 * * *' // Täglich um 8:00 } }); } ``` --- ## Worker implementieren ```typescript // workers/email.worker.ts import { Worker, Job } from 'bullmq'; import IORedis from 'ioredis'; import { Resend } from 'resend'; import { EmailJob } from '@/types/jobs'; const connection = new IORedis(process.env.REDIS_URL!, { maxRetriesPerRequest: null }); const resend = new Resend(process.env.RESEND_API_KEY); const emailWorker = new Worker( 'email', async (job: Job) => { console.log(`Processing job ${job.id}: ${job.data.type}`); const { to, subject, template, data } = job.data; // Template rendern const html = await renderTemplate(template, data); // Email senden const result = await resend.emails.send({ from: 'noreply@example.com', to, subject, html }); console.log(`Email sent: ${result.id}`); return { emailId: result.id }; }, { connection, concurrency: 5, // 5 Jobs parallel limiter: { max: 100, // Max 100 Jobs duration: 60000 // Pro Minute (Rate Limiting) } } ); // Event Handlers emailWorker.on('completed', (job, result) => { console.log(`Job ${job.id} completed with result:`, result); }); emailWorker.on('failed', (job, error) => { console.error(`Job ${job?.id} failed:`, error.message); }); emailWorker.on('progress', (job, progress) => { console.log(`Job ${job.id} progress: ${progress}%`); }); export { emailWorker }; ``` ```typescript // workers/image.worker.ts import { Worker, Job } from 'bullmq'; import sharp from 'sharp'; import { ImageProcessingJob } from '@/types/jobs'; const imageWorker = new Worker( 'image-processing', async (job: Job) => { const { imageId, operations, outputFormat } = job.data; // Bild laden const image = await loadImage(imageId); let pipeline = sharp(image); // Operationen anwenden for (let i = 0; i < operations.length; i++) { const op = operations[i]; // Progress updaten await job.updateProgress(Math.round((i / operations.length) * 100)); switch (op.type) { case 'resize': pipeline = pipeline.resize(op.params.width, op.params.height); break; case 'crop': pipeline = pipeline.extract(op.params); break; case 'watermark': pipeline = pipeline.composite([{ input: await loadWatermark(), gravity: 'southeast' }]); break; } } // Output const output = await pipeline[outputFormat]().toBuffer(); // Speichern const url = await uploadToStorage(output, `${imageId}.${outputFormat}`); return { url, size: output.length }; }, { connection, concurrency: 2 // CPU-intensive, weniger parallel } ); ``` --- ## Job Progress & Events ```typescript // API Route: Job Status // app/api/jobs/[id]/route.ts import { emailQueue } from '@/lib/queue'; export async function GET( request: Request, { params }: { params: { id: string } } ) { const job = await emailQueue.getJob(params.id); if (!job) { return Response.json({ error: 'Job not found' }, { status: 404 }); } const state = await job.getState(); const progress = job.progress; return Response.json({ id: job.id, state, progress, data: job.data, returnValue: job.returnvalue, failedReason: job.failedReason, timestamp: job.timestamp, processedOn: job.processedOn, finishedOn: job.finishedOn }); } ``` ```typescript // Real-time Updates mit SSE // app/api/jobs/[id]/stream/route.ts import { emailQueueEvents } from '@/lib/queue'; export async function GET( request: Request, { params }: { params: { id: string } } ) { const encoder = new TextEncoder(); const stream = new ReadableStream({ start(controller) { const handleCompleted = ({ jobId, returnvalue }) => { if (jobId === params.id) { controller.enqueue( encoder.encode(`data: ${JSON.stringify({ event: 'completed', result: returnvalue })}\n\n`) ); controller.close(); } }; const handleFailed = ({ jobId, failedReason }) => { if (jobId === params.id) { controller.enqueue( encoder.encode(`data: ${JSON.stringify({ event: 'failed', error: failedReason })}\n\n`) ); controller.close(); } }; const handleProgress = ({ jobId, data }) => { if (jobId === params.id) { controller.enqueue( encoder.encode(`data: ${JSON.stringify({ event: 'progress', progress: data })}\n\n`) ); } }; emailQueueEvents.on('completed', handleCompleted); emailQueueEvents.on('failed', handleFailed); emailQueueEvents.on('progress', handleProgress); return () => { emailQueueEvents.off('completed', handleCompleted); emailQueueEvents.off('failed', handleFailed); emailQueueEvents.off('progress', handleProgress); }; } }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' } }); } ``` --- ## Job Scheduling (Cron) ```typescript // Wiederkehrende Jobs einrichten async function setupScheduledJobs() { // Täglich um 8:00 UTC await reportQueue.add('daily-report', { reportType: 'daily' }, { repeat: { pattern: '0 8 * * *' }, jobId: 'daily-report' // Verhindert Duplikate }); // Wöchentlich Montag 9:00 await reportQueue.add('weekly-report', { reportType: 'weekly' }, { repeat: { pattern: '0 9 * * 1' }, jobId: 'weekly-report' }); // Alle 5 Minuten await emailQueue.add('process-email-queue', {}, { repeat: { every: 5 * 60 * 1000 } }); } // Scheduled Jobs verwalten async function listScheduledJobs() { const repeatableJobs = await emailQueue.getRepeatableJobs(); return repeatableJobs; } async function removeScheduledJob(key: string) { await emailQueue.removeRepeatableByKey(key); } ``` --- ## Dashboard & Monitoring ```typescript // Bull Board Integration import { createBullBoard } from '@bull-board/api'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; import { ExpressAdapter } from '@bull-board/express'; const serverAdapter = new ExpressAdapter(); serverAdapter.setBasePath('/admin/queues'); createBullBoard({ queues: [ new BullMQAdapter(emailQueue), new BullMQAdapter(imageQueue), new BullMQAdapter(reportQueue) ], serverAdapter }); // In Express/Hono einbinden app.use('/admin/queues', serverAdapter.getRouter()); ``` --- ## Best Practices ```typescript // 1. Idempotente Jobs (können mehrfach ausgeführt werden) async function processOrder(job: Job<{ orderId: string }>) { const order = await db.order.findUnique({ where: { id: job.data.orderId } }); // Prüfen ob schon verarbeitet if (order?.status === 'processed') { return { skipped: true, reason: 'Already processed' }; } // Verarbeiten await db.order.update({ where: { id: order.id }, data: { status: 'processed' } }); } // 2. Graceful Shutdown process.on('SIGTERM', async () => { console.log('Shutting down workers...'); await emailWorker.close(); await imageWorker.close(); process.exit(0); }); // 3. Dead Letter Queue const dlQueue = new Queue('dead-letter', { connection }); emailWorker.on('failed', async (job, error) => { if (job.attemptsMade >= job.opts.attempts!) { await dlQueue.add('failed-email', { originalJob: job.data, error: error.message, failedAt: new Date() }); } }); ``` --- ## Fazit BullMQ bietet: 1. **Zuverlässigkeit**: Retries, Persistence, Acknowledgement 2. **Skalierbarkeit**: Mehrere Worker, Rate Limiting 3. **Flexibilität**: Scheduling, Prioritäten, Progress 4. **Monitoring**: Bull Board, Events, Metrics Unverzichtbar für Production-Grade Async Processing. --- ## Bildprompts 1. "Queue of tasks being processed by workers, assembly line concept" 2. "Background process running while user continues, async workflow" 3. "Redis connecting multiple workers, distributed job processing" --- ## Quellen - [BullMQ Documentation](https://docs.bullmq.io/) - [BullMQ GitHub](https://github.com/taskforcesh/bullmq) - [Bull Board](https://github.com/felixmosh/bull-board) - [Redis Pub/Sub](https://redis.io/docs/latest/develop/interact/pubsub/)