DDevArchive
Đăng nhập

Background jobs: BullMQ, cron, retry & dead letter queue

Gửi email, resize ảnh, generate PDF, tính billing hàng tháng — tất cả KHÔNG ĐƯỢC chạy trong request handler. Chạy sync = user đợi 10 giây, timeout, server chết khi load cao. Background job = chạy riêng, retry tự động, user không bị ảnh hưởng.

Request handler vs Background job

Request handler (sync)Background job (async)
User đợi đến khi xongUser nhận “Đang xử lý…” ngay lập tức
Timeout sau 30 giâyChạy bao lâu cũng được
Fail = user thấy lỗiFail = retry tự động, user không biết
1000 request = 1000 lần gửi email đồng thời1000 job → queue → worker xử lý từng batch

BullMQ: job queue cho Node.js

// File: email-queue.ts
import { Queue, Worker } from 'bullmq'
import { sendEmail } from './mailer'

// 1. Tạo queue
const emailQueue = new Queue('emails', {
  connection: { host: 'localhost', port: 6379 },
})

// 2. Thêm job vào queue (trong API handler)
async function onUserSignup(user: User) {
  await emailQueue.add('welcome', {
    to: user.email,
    name: user.name,
  }, {
    attempts: 3,           // retry 3 lần nếu fail
    backoff: {
      type: 'exponential', // 1s → 2s → 4s
      delay: 1000,
    },
  })
  // User nhận response NGAY, email gửi sau
}

// 3. Worker xử lý job (chạy process riêng)
const worker = new Worker('emails', async (job) => {
  await sendEmail({
    to: job.data.to,
    subject: `Chào ${job.data.name}!`,
    template: 'welcome',
  })
}, {
  connection: { host: 'localhost', port: 6379 },
  concurrency: 5, // xử lý 5 email cùng lúc
})

// 4. Xử lý job fail sau tất cả retry
worker.on('failed', (job, err) => {
  logger.error('Email permanently failed', {
    jobId: job?.id,
    to: job?.data.to,
    error: err.message,
  })
  // → Alert team, hoặc move to dead letter queue
})

Cron jobs: lịch chạy tự động

// File: cron-jobs.ts
import { Queue, QueueScheduler } from 'bullmq'

// Billing hàng tháng: chạy 00:00 ngày 1 mỗi tháng
await billingQueue.add('monthly-billing', {}, {
  repeat: { pattern: '0 0 1 * *' },
})

// Cleanup expired sessions: mỗi 6 giờ
await cleanupQueue.add('cleanup-sessions', {}, {
  repeat: { pattern: '0 */6 * * *' },
})

// Daily analytics report: 8:00 sáng mỗi ngày
await reportQueue.add('daily-report', {}, {
  repeat: { pattern: '0 8 * * *' },
})

Dead Letter Queue: khi job chết hẳn

Job fail → retry 3 lần → vẫn fail → Dead Letter Queue (DLQ). DLQ giữ job thất bại để bạn inspect, debug, replay. Không có DLQ = job biến mất → bạn không bao giờ biết 500 email chưa gửi.

Idempotency trong background jobs

Job có thể chạy LẠI (retry). Nếu job “tạo invoice” chạy 2 lần → user nhận 2 invoice. Solution: mỗi job có unique key (idempotency key). Trước khi xử lý → check đã xử lý chưa → skip nếu rồi.

❓ Khi nào PHẢI dùng background job thay vì xử lý sync?

  • Setup BullMQ với Redis
  • Tách email gửi ra background job
  • Cấu hình retry 3 lần + exponential backoff
  • Setup dead letter queue cho job fail vĩnh viễn