Setiap backend akhirnya menumbuhkan pekerjaan yang tidak seharusnya terjadi di dalam request HTTP: merender PDF invoice 40 halaman, mengirim email massal, sinkronisasi stok ke API marketplace yang membatasi rate Anda. Versi pertamanya selalu await di dalam controller, dan akhirnya selalu sama: pengguna menatap spinner 20 detik, lalu gateway timeout, lalu submit ganda. Background job bukan optimisasi. Melewati durasi pekerjaan tertentu, ia satu-satunya arsitektur yang benar.
Stack saya untuk ini di dunia Node.js adalah BullMQ di atas Redis, disambungkan ke NestJS. BullMQ adalah library queue matang yang dibangun di atas Redis dengan operasi atomik berbasis skrip Lua, dan ia menutupi empat hal yang benar-benar dituntut production: retry dengan backoff, parkir kegagalan, kontrol konkurensi, dan observability. Tulisan ini adalah setup yang saya deploy untuk beban kerja ERP, dengan pelajaran operasional yang dilewatkan panduan quickstart.
Apa yang Diberikan Queue Dibanding Async Call Naif
Melempar promise tanpa await di dalam request handler terasa seperti background job, tapi ia hidup dan mati bersama prosesnya. Deploy, crash, atau scale down, dan pekerjaannya menguap diam-diam. Queue berbasis Redis memberi job kehidupannya sendiri:
- Durabilitas: job dipersist di Redis sejak di-enqueue, jadi restart worker atau deploy tidak menghilangkannya; job yang belum selesai diambil lagi.
- Kebijakan retry sebagai konfigurasi: attempts dan backoff dideklarasikan per queue atau per job, bukan dirakit manual dengan setTimeout di tujuh belas tempat.
- Backpressure dan konkurensi: delapan render PDF sekaligus alih-alih tiga ratus, secepat apa pun pengguna mengklik, sehingga mesin tetap hidup.
- Observability: setiap job punya state yang bisa diperiksa, dihitung, dan digrafikkan; waiting, active, completed, failed, delayed. Promise yang mengambang tidak punya apa-apa.
Setup NestJS yang Benar-Benar Saya Deploy
Integrasi resmi NestJS memakai BullModule dari paket rasa bullmq. Konfigurasi koneksi tinggal di root; tiap feature module mendaftarkan queue-nya sendiri dengan opsi job default sehingga kebijakan retry dimiliki queue, bukan tercecer di para producer:
// app.module.ts — connection once, queues per feature
BullModule.forRoot({
connection: { host: process.env.REDIS_HOST, port: 6379 },
})
BullModule.registerQueue({
name: 'invoices',
defaultJobOptions: {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: { age: 86400, count: 5000 },
removeOnFail: false, // keep failures for inspection
},
})
// invoices.processor.ts
@Processor('invoices', { concurrency: 8 })
export class InvoicesProcessor extends WorkerHost {
async process(job: Job<GenerateInvoiceDto>) {
switch (job.name) {
case 'generate-pdf':
return this.pdf.render(job.data.invoiceId)
case 'send-email':
return this.mailer.sendInvoice(job.data.invoiceId)
}
}
}Tiga default di snippet itu bekerja luar biasa keras. Pasangan attempts dan exponential backoff menangani kegagalan transien secara otomatis. Kebijakan removeOnComplete dengan batas umur menjaga memori Redis datar alih-alih tumbuh selamanya, yang merupakan cara paling umum tim tanpa sengaja memenuhkan instance Redis. Dan removeOnFail bernilai false mengawetkan kegagalan terminal, karena kegagalan yang tidak bisa diperiksa adalah kegagalan yang tidak bisa diperbaiki.
Retry dan Backoff: Jadikan Kegagalan Membosankan
BullMQ me-retry job yang gagal saat attempts lebih dari satu, dan setelan backoff mengatur jaraknya. Strategi bawaannya fixed, yang menunggu delay sama setiap kali, dan exponential, yang menggandakan waktu tunggu per percobaan memakai formula terdokumentasi dua pangkat jumlah percobaan dikurangi satu, dikalikan delay dasar. Dengan delay dasar 2 detik dan lima attempts, jadwalnya menyimpang seperti ini:
| Percobaan | Backoff fixed (2 dtk) | Backoff exponential (basis 2 dtk) |
|---|---|---|
| 1 | langsung (eksekusi pertama) | langsung (eksekusi pertama) |
| 2 | setelah tunggu 2 dtk | setelah tunggu 2 dtk |
| 3 | setelah tunggu 2 dtk | setelah tunggu 4 dtk |
| 4 | setelah tunggu 2 dtk | setelah tunggu 8 dtk |
| 5 | setelah tunggu 2 dtk (total tunggu ~8 dtk) | setelah tunggu 16 dtk (total tunggu ~30 dtk) |
Exponential adalah default yang benar untuk apa pun yang memanggil sistem eksternal: kalau API marketplace tumbang semenit, menggedornya tiap 2 detik hanya membakar rate limit Anda, sedangkan jarak eksponensial melewati outage itu dengan tenang. Backoff fixed saya sisakan untuk pekerjaan murni internal seperti resize gambar, yang penyebab gagalnya biasanya lonjakan resource sesaat. Anda juga bisa mendaftarkan strategi backoff kustom untuk kasus khusus, seperti membaca header retry-after dari respons 429 dan mematuhinya persis.
Retry berarti processor Anda akan berjalan lebih dari sekali untuk pekerjaan logis yang sama; itu kontrak yang Anda tanda tangani. Setiap handler harus idempotent: render PDF ke path yang deterministik, upsert alih-alih insert, cek apakah pengiriman email sudah tercatat sebelum mengirim. Queue menjamin job berjalan setidaknya sekali sampai sukses; hanya handler Anda yang bisa membuat pengulangan tidak berbahaya.
Dead-Letter Queue: Tempat Job Menunggu Di-debug
Sebagian kegagalan bukan transien. Payload cacat, record terhapus, bug di handler: semuanya akan gagal di percobaan kelima persis seperti percobaan pertama. Tanpa rencana, mereka duduk di set failed selamanya atau, lebih buruk, di-retry dalam loop tak berujung yang menenggelamkan error sungguhan dalam kebisingan. Pola yang berhasil adalah dead-letter queue: saat job menghabiskan attempts-nya, handler event worker memindahkannya ke queue terpisah dengan konteks lengkap dan memanggil manusia:
// Listen for jobs that exhausted all attempts
@OnWorkerEvent('failed')
onFailed(job: Job, err: Error) {
if (job.attemptsMade >= (job.opts.attempts ?? 1)) {
// terminal failure → park it in a DLQ for humans
this.dlqQueue.add('dead-letter', {
source: 'invoices',
jobName: job.name,
data: job.data,
error: err.message,
failedAt: new Date().toISOString(),
})
this.alerts.notify('Invoice job dead-lettered: ' + job.id)
}
}
// Replay later, after the bug is fixed:
// pull from DLQ → re-add to the original queue with attempts resetDLQ menjadi inbox triase dengan tiga hasil per entri: replay setelah perbaikan, buang karena usang, atau ubah jadi tiket bug. Di satu deployment ERP, pola tunggal ini memangkas waktu rata-rata kami menyadari integrasi rusak dari hitungan hari, saat pelanggan komplain, menjadi hitungan menit, saat alert berbunyi. Jalur replay sama pentingnya dengan jalur parkir: jaga payload job kecil dan mandiri, ID alih-alih objek utuh, supaya dead letter berumur seminggu tetap bisa diputar ulang dengan benar terhadap data terkini.
Empat Pola yang Membayar Sewa di Production
ID di payload, bukan objek
Enqueue ID invoice, bukan invoice-nya. Worker mengambil state segar saat eksekusi, jadi job yang tertunda retry tidak bertindak atas snapshot basi. Memori Redis juga jadi terprediksi.
Deduplikasi lewat job ID
BullMQ memperlakukan jobId kustom sebagai unik selama job-nya masih ada: menambah ID sama lagi akan diabaikan. Pakai ID deterministik seperti sync-stock-sku123 untuk meringkas pemicu duplikat jadi satu eksekusi.
Job delayed dan repeatable
Pengingat pembayaran tiga hari sebelum jatuh tempo adalah delayed job. Rekonsiliasi stok tiap malam adalah repeatable job dengan pola cron. Keduanya menggantikan rangkaian rapuh cron eksternal plus HTTP dengan satu sistem konsisten.
Queue terpisah per radius ledakan
Email, PDF, dan sinkronisasi eksternal masing-masing dapat queue dengan konkurensinya sendiri. Satu integrasi yang macet jadi tidak bisa membuat pembuatan invoice kelaparan, dan Anda bisa menjeda satu queue saja saat insiden.
Monitoring: Queue Itu Berbohong Sampai Anda Grafikkan
Queue yang tampak baik-baik saja di development punya dua mode kegagalan production yang harus Anda lihat datang: backlog yang tumbuh, saat producer melampaui consumer, dan kematian worker yang sunyi, saat tidak ada yang mengkonsumsi sama sekali. Saya mengekspor tiga angka per queue ke Prometheus dan memasang alert di Grafana:
- Jumlah waiting dan trennya: backlog yang tumbuh 15 menit berturut-turut berarti consumer kurang atau macet; alert sebelum pengguna merasakan latency.
- Delta jumlah failed: lebih dari N kegagalan baru dalam 10 menit adalah sinyal insiden, biasanya perubahan API downstream, bukan N kebetulan independen.
- Heartbeat worker: gauge yang di-update tiap worker setiap 30 detik. Kalau ia basi sementara jumlah waiting naik, prosesnya mati atau tersangkut job beracun, dan perbedaan itu menentukan apakah Anda restart atau menggali.
Jalankan worker BullMQ sebagai proses terpisah dari API Anda, bahkan di VPS yang sama. Worker yang mengunyah render PDF berat CPU tidak boleh bersaing dengan latency request, dan Anda mendapat deploy serta restart yang independen. Di Docker Compose atau Swarm itu cuma service kedua yang memakai image sama dengan command berbeda.
Checklist Production
- Default diset per queue: attempts 3 sampai 5, exponential backoff, removeOnComplete dengan batas umur, removeOnFail dipertahankan untuk inspeksi.
- Setiap processor idempotent dan payload membawa ID, bukan snapshot.
- Handler dead-letter pada event failed dengan alerting dan prosedur replay yang terdokumentasi.
- Queue dipisah per domain kegagalan dengan konkurensi eksplisit per worker.
- Metrik waiting, failed, dan heartbeat digrafikkan dan di-alert; Redis dikonfigurasi dengan persistence dan maxmemory-policy noeviction supaya penyimpanan queue tidak pernah diam-diam membuang job.
Queue Adalah Investasi untuk Operasional yang Membosankan
Perbedaan antara sistem background job yang Anda percaya dan yang Anda takuti bukan di jalur bahagia; tutorial mana pun bisa mengantar job dari producer ke processor dalam 20 baris. Bedanya ada di hari buruk: API marketplace tumbang sejam, deploy mendarat di tengah job, payload beracun tiba jam 2 pagi. Attempts dan backoff menyerap kegagalan transien, dead-letter queue menangkap yang permanen, handler idempotent membuat retry tidak berbahaya, dan metrik memberi tahu Anda semuanya sebelum pengguna Anda. Siapkan empat hal itu sejak hari pertama dan BullMQ akan membalas Anda dengan bertahun-tahun job yang tidak pernah Anda pikirkan lagi.
Sumber dan bacaan lanjutan

