Request handler vs Background job
| Request handler (sync) | Background job (async) |
|---|---|
| User đợi đến khi xong | User nhận “Đang xử lý…” ngay lập tức |
| Timeout sau 30 giây | Chạy bao lâu cũng được |
| Fail = user thấy lỗi | Fail = retry tự động, user không biết |
| 1000 request = 1000 lần gửi email đồng thời | 1000 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.
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