Menjalankan pengubahan ukuran gambar di dalam rute Express adalah resep menuju bencana. Seorang pengguna mengunggah foto berukuran sepuluh megabyte, server Anda mulai memproses piksel, dan tiga puluh detik kemudian permintaan tersebut mengalami timeout. Antrean pekerjaan latar belakang (background job queues) ada justru untuk mencegah rasa sakit semacam ini. Dalam ekosistem Node.js, Bull dan BullMQ telah menjadi dua pemain berat untuk menangani pekerjaan asinkron melalui Redis. Keduanya berbagi DNA yang sama tetapi sangat berbeda dalam hal filosofi dan ergonomi penggunaan sehari-hari. Memilih yang tepat sangatlah penting karena beralih di kemudian hari bukanlah sekadar pembaruan paket yang sederhana.

Fondasi yang Sama

Kedua pustaka ini menggunakan Redis sebagai tulang punggungnya. Redis menangani operasi atomik, sorted sets untuk pekerjaan yang ditunda, dan pub/sub untuk peristiwa (events). Jika Anda sudah menjalankan Redis untuk caching atau sesi, menambahkan antrean pekerjaan tidak memerlukan infrastruktur baru. Baik Bull maupun BullMQ mendukung prioritas, percobaan ulang dengan backoff, kontrol konkurensi, dan pekerjaan yang dapat diulang (repeatable jobs). Tumpang tindih tersebut membuat pilihan menjadi lebih sulit, bukan lebih mudah. Anda tidak bisa hanya mengandalkan daftar fitur. Sebaliknya, Anda harus melihat bagaimana setiap pustaka ingin Anda menyusun kode Anda.

Bull: Sang Veteran yang Teruji

Bull telah ada selama bertahun-tahun dan berjalan di ribuan aplikasi produksi. Ini berfungsi dengan baik. API-nya membungkus segalanya ke dalam satu instans Queue. Anda membuat instansnya, menentukan fungsi pemrosesan, dan mendengarkan peristiwa, semuanya pada objek yang sama. Desain monolitik ini terasa akrab jika Anda datang dari pola Node.js yang lebih lama. Basis kode yang ada sebelum penggunaan async/await yang luas sangat cocok dengan Bull karena ia berkembang bersamaan dengan callback dan klien Redis versi awal.

Kekurangannya adalah keterikatan yang erat (tight coupling). Saat server API Anda membuat sebuah pekerjaan, ia mengimpor objek Queue yang sama yang berisi logika worker. Dalam praktiknya, ini berarti proses web Anda ikut menarik dependensi yang tidak pernah dieksekusinya. Ini bukan cacat fatal, tetapi mengganggu arsitektur yang bersih. Untuk beban kerja sederhana, Anda mungkin tidak akan menyadarinya. Untuk tim besar dengan puluhan modul, gesekan tersebut akan menumpuk.

BullMQ: Dibangun Ulang dari Nol

BullMQ adalah penerus resminya. Ia ditulis ulang dalam TypeScript sejak hari pertama, sehingga tipe data (types) bukanlah pemikiran tambahan yang ditempelkan ke sumber JavaScript. API-nya membagi tanggung jawab ke dalam kelas-kelas yang berbeda. Queue menangani penambahan pekerjaan. Worker menangani pemrosesannya. QueueEvents menangani observabilitas. Pemisahan ini mencerminkan bagaimana sistem terdistribusi modern sebenarnya beroperasi. Pod API Anda hanya memerlukan kelas Queue dan koneksi Redis. Pod worker Anda mengimpor kelas Worker. Batasannya bersifat fisik, bukan sekadar konseptual.

Perubahan ini membuahkan hasil pada tim besar. Seorang pengembang yang merilis fitur baru dapat memasukkan pekerjaan ke antrean tanpa perlu tahu file mana yang berisi pemrosesnya. Kompiler menangkap ketidakcocokan tipe antara data pekerjaan dan handler lebih awal, alih-alih saat runtime. API async/await juga terasa asli (native) di Node.js modern. Anda tidak akan merasa sedang melawan konvensi lama.

Alur Pekerjaan: Dari Sekadar Trik Menjadi Warga Kelas Satu

Alur kerja multi-langkah menunjukkan celah terlebar antara kedua pustaka tersebut.

Misalkan Anda sedang membangun alur penagihan (invoicing) e-commerce. Seorang pelanggan melakukan checkout. Anda perlu memesan inventaris, menagih kartu, membuat PDF, dan mengirim email. Dengan Bull, merantai langkah-langkah ini berarti melakukan pembukuan manual. Anda mungkin membuat satu pemroses memicu pekerjaan berikutnya, meneruskan status melalui Redis atau muatan data (data payloads) yang besar. Anda menulis koordinasi induk-anak (parent-child) sendiri. Ini berhasil sampai akhirnya tidak. Logika percobaan ulang (retry) menjadi berantakan. Jika langkah PDF gagal, membatalkan penagihan memerlukan kode kompensasi khusus yang mudah salah.

BullMQ memperkenalkan FlowProducer. Anda menentukan pohon pekerjaan di mana induk secara otomatis menunggu anaknya. Dalam contoh penagihan, Anda membuat pekerjaan akar bernama finalize-order dengan tiga anak: reserve-inventory, charge-payment, dan generate-pdf. Anda dapat menjadikan notifikasi email sebagai anak dari pekerjaan PDF. Redis menyimpan struktur graf tersebut. Induk hanya akan aktif ketika setiap dependensi berhasil. Jika satu anak gagal, seluruh cabang akan terhenti. Anda tidak perlu menulis loop polling atau pembuat pekerjaan rekursif. Ini bukan sekadar syntactic sugar. Ini mengubah cara Anda memodelkan logika bisnis.

Pembatasan Laju (Rate Limiting): Instrumen Tumpul vs. Pisau Bedah

Kedua pustaka dapat membatasi throughput, tetapi granularitasnya sangat berbeda.

Bull menerapkan rate limit per antrean. Jika Anda mengatur antrean untuk memproses seratus pekerjaan per detik, batas tersebut berlaku sama untuk setiap pekerjaan di dalam antrean. Ini tidak masalah untuk beban kerja yang homogen. Namun, hal ini bermasalah pada platform SaaS multitenant. Bayangkan satu pelanggan yang "berisik" mengirimkan satu juta pengiriman webhook ke dalam antrean bersama. Batasan tingkat antrean pada Bull berarti Anda tidak dapat memperlambat tenant tersebut tanpa memperlambat semua orang lainnya. Pilihan Anda sangat sulit: buat antrean Redis terpisah untuk setiap pelanggan dan kelola secara dinamis, atau terima ketidakadilan tersebut.

BullMQ menambahkan pembatasan laju berbasis grup. Anda menandai setiap pekerjaan dengan kunci grup, biasanya ID tenant atau pengguna, dan menentukan batas per grup. Antrean yang sama memproses pekerjaan untuk semua tenant, tetapi penjadwal (scheduler) membatasi setiap grup secara independen. Lonjakan dari Pelanggan A tidak akan menghambat Pelanggan B. Anda menghindari pembengkakan jumlah antrean (queue sprawl) dan menjaga keyspace Redis tetap rapi. Bagi platform dengan kekhawatiran akan masalah noisy-neighbor, fitur ini saja sudah cukup untuk membenarkan migrasi.

Arsitektur yang Lebih Bersih dalam Praktik

Pemisahan antara Queue dan Worker mungkin terasa samar sampai Anda melakukan debug pada insiden produksi. Dengan Bull, umum terjadi melihat kode pembuatan pekerjaan tertanam jauh di dalam route handler yang juga mengimpor dependensi pemrosesan yang berat. BullMQ memaksa Anda untuk memutuskan di mana pekerjaan dilakukan. Server web Anda tetap ringan. Kontainer worker Anda membundel pustaka berat, pemroses gambar, atau headless browser. Jika terjadi kebocoran memori, Anda tahu persis jenis proses mana yang harus di-profile. Model mentalnya lebih mirip dengan sistem seperti Celery atau Sidekiq.

Membuat Pilihan

Mulailah dengan BullMQ jika Anda sedang membangun dari nol. Definisi TypeScript-nya akurat dan lengkap. Alur pekerjaan (job flows) menghilangkan tumpukan kode orkestrasi. Pembatasan laju berbasis grup menyelesaikan masalah keadilan sebelum masalah itu muncul. API async/await terasa sangat natural. Tidak banyak alasan untuk memilih pustaka yang lebih lama untuk proyek greenfield.

Tetaplah gunakan Bull jika sistem Anda sudah berjalan dengan baik. Migrasi memakan waktu dan berisiko terhadap stabilitas. Jika pekerjaan Anda bersifat datar dan independen, Anda tidak kehilangan fitur yang sebenarnya Anda butuhkan. Antrean yang hanya mengirim email reset kata sandi dan mengubah ukuran avatar tidak memerlukan grafik alur (flow graphs). Menulis ulang kode yang sudah berjalan demi kemurnian teoretis bukanlah rekayasa (engineering). Itu hanyalah hobi.

Pemeriksaan Realitas Migrasi

Jika Anda memutuskan untuk beralih, perlakukan hal tersebut sebagai perubahan infrastruktur, bukan sekadar refaktor kode. Bull dan BullMQ menggunakan skema kunci Redis yang berbeda. Keduanya tidak dapat membaca data atau status pekerjaan satu sama lain. Anda tidak bisa sekadar mengaktifkan feature flag dan berharap pekerjaan lama akan selesai. Anda harus mengosongkan setiap antrean yang ada hingga nol, menerapkan worker baru, dan mulai memasukkan antrean dengan BullMQ. Rencanakan jendela pemeliharaan (maintenance window) atau blue-green deployment di mana worker lama mengonsumsi antrean lama sementara worker baru menangani antrean yang baru.

Kesimpulan Utama