Saya pakai PgBoss di kantor dan pengin paham implementasinya. Saya membandingkannya dengan BullMQ. Keduanya adalah job queue untuk Node.js, tapi yang satu menyimpan state queue di PostgreSQL dan yang lain di Redis. Repository PgBoss 12.27.0 dan dokumentasi BullMQ menjelaskan tujuan besar yang sama, dengan model storage dan koordinasi yang berbeda.
Masalah yang diselesaikan keduanya
Kamu punya pekerjaan yang harus jalan secara asynchronous. Kirim email setelah user sign up. Proses file rekonsiliasi pembayaran. Buat report. Resize gambar yang di-upload. Apa pun itu, kamu nggak mau mengerjakannya di dalam siklus request/response.
Kamu butuh:
- Reliability: job nggak boleh hilang kalau worker crash
- Concurrency: banyak worker harus bisa memproses job secara paralel tanpa menduplikasi pekerjaan
- Scheduling: sebagian job harus jalan di waktu tertentu atau berulang dengan interval tertentu
- Retry logic: job yang gagal harus di-retry dengan backoff
- Ordering: sebagian job perlu diproses secara berurutan (per customer, per akun, dan seterusnya)
Kedua library ini menangani kebutuhan tersebut, tapi jaminan dan biaya operasionalnya bergantung pada model storage dan konfigurasinya.
PgBoss: PostgreSQL sebagai job queue
PgBoss menyimpan job di tabel PostgreSQL, jadi aplikasi yang udah menjalankan PostgreSQL nggak butuh queue store terpisah. Dokumentasi tabel job menunjukkan field untuk state queue, payload, retry, dan retention.
Cara kerjanya
graph LR
P[Producer] -->|INSERT| T[(pgboss.job table)]
T -->|SELECT ... FOR UPDATE SKIP LOCKED| W1[Worker 1]
T -->|SELECT ... FOR UPDATE SKIP LOCKED| W2[Worker 2]
T -->|SELECT ... FOR UPDATE SKIP LOCKED| W3[Worker 3]
Pembuatan job cuma sebuah INSERT:
import { PgBoss } from "pg-boss";
const boss = new PgBoss("postgres://localhost/mydb");
await boss.start();
// Create a job
await boss.send("send-email", {
to: "[email protected]",
subject: "Welcome!",
body: "Thanks for signing up.",
});
Di dalamnya, PgBoss meng-insert satu row ke tabel job-nya dengan nama queue, payload, state, dan metadata scheduling.
Pengambilan job memakai SELECT ... FOR UPDATE SKIP LOCKED di backend PostgreSQL default. Berikut ini query claim yang disederhanakan, bukan statement PgBoss yang lengkap:
SELECT id, data
FROM pgboss.job
WHERE name = 'send-email'
AND state < 'active'
AND start_after <= now()
ORDER BY priority DESC, created_on, id
LIMIT 1
FOR UPDATE SKIP LOCKED;
FOR UPDATE me-lock row yang dipilih selama transaction berjalan. Opsi SKIP LOCKED di PostgreSQL membuat worker bisa melewati row yang sedang dipegang transaction lain, dan ini berguna untuk consumer queue. Claim dan update state tetap jalan di dalam transaction, sedangkan perilaku retry atau recovery ditangani terpisah oleh PgBoss. Lihat dokumentasi locking clause PostgreSQL.
sequenceDiagram
participant W1 as Worker 1
participant DB as PostgreSQL
participant W2 as Worker 2
W1->>DB: SELECT ... FOR UPDATE SKIP LOCKED
Note over DB: Row 1 locked by W1
W2->>DB: SELECT ... FOR UPDATE SKIP LOCKED
Note over DB: Row 1 skipped (locked), Row 2 returned
W1->>DB: UPDATE state = 'completed'
W1->>DB: COMMIT (lock released)
W2->>DB: UPDATE state = 'completed'
W2->>DB: COMMIT (lock released)
Penyelesaian job meng-update state row menjadi completed atau failed lalu meng-commit transaction-nya, sehingga lock pada row dilepas.
Bagian dalam PgBoss
Bagian utama yang perlu diingat:
Maintenance dan retention: Secara berkala PgBoss mengawasi queue, meng-expire job yang timeout atau yang heartbeat-nya udah basi, dan menghapus row sesuai pengaturan retention queue. Job yang udah selesai disimpan di tabel job sampai deleteAfterSeconds, bukan disalin ke tabel arsip terpisah. Opsi job dan definisi tabel job mendokumentasikan field-field tersebut.
Koordinasi schema: PgBoss versi sekarang memakai pg_advisory_xact_lock dengan scope transaction waktu membuat atau me-migrate schema-nya. Ini beda dengan supervisor yang mengecek queue. Dokumentasi database backend menjelaskan peran lock ini.
// Simplified schema-migration coordination
await db.query("SELECT pg_advisory_xact_lock($1)", [SCHEMA_LOCK_KEY]);
Exponential backoff: Job yang gagal bisa di-retry dengan delay dan backoff yang bisa dikonfigurasi. Field retry_delay, retry_backoff, dan retry_count di tabel job menyimpan state yang relevan.
Policy job dan scheduling: Policy queue mencakup throttling, perilaku singleton, prioritas, dan concurrency. Job yang berulang memakai cron expression dan dievaluasi oleh scheduler yang dijelaskan di scheduling API.
Di mana PostgreSQL menambah beban
Setiap enqueue dan transisi state adalah write ke PostgreSQL. Queue bisa berbagi transaction dan tool backup dengan aplikasi. Traffic queue juga bersaing dengan traffic aplikasi untuk resource database. Perhatikan:
- Volume WAL dan disk I/O dari insert dan perubahan state. PostgreSQL mencatat perubahan di Write Ahead Log, sementara durability commit tetap mengikuti pengaturan WAL dan synchronous commit di database.
- Row yang disimpan bisa ikut membuat tabel dan index membengkak. Retention dan partitioning membatasi cakupannya.
- Tekanan vacuum dari update state job yang sering
- Tekanan connection pool dari polling worker dan ukuran batch
Repository ini nggak punya benchmark PgBoss dengan versi, hardware, topologi, payload, dan workload yang bisa diulang. Saya nggak bisa menentukan batas throughput dari materi ini.
Ukur enqueue rate, claim rate, completion rate, queue depth, dan pickup latency worker di deployment target. Ukur juga CPU database, volume WAL, lock wait, dan aktivitas vacuum.
BullMQ: Redis sebagai job queue
BullMQ menyimpan state queue di Redis dan mengoordinasikan operasi queue dengan struktur data Redis dan script atomic. Panduan arsitektur BullMQ menjelaskan state wait, prioritized, delayed, active, completed, dan failed.
Cara kerjanya
graph LR
P[Producer] -->|Lua add script| Q[(Redis job keys and wait list)]
Q -->|Worker wait and moveToActive script| A[Active state]
A --> W1[Worker 1]
A --> W2[Worker 2]
W1 -->|Lua finish script| F[(Completed or failed sets)]
BullMQ memakai beberapa struktur data Redis yang bekerja bersama:
- Wait queue (Redis list): job baru di-push ke sini.
- Active state: job yang sedang diproses.
- Delayed set (Redis sorted set): job yang dijadwalkan untuk nanti, dengan score berupa waktu eksekusinya.
- Completed dan failed set: job yang udah selesai disimpan untuk tracking, tergantung opsi cleanup queue.
Pembuatan job:
import { Queue, Worker } from "bullmq";
const queue = new Queue("send-email", {
connection: { host: "localhost", port: 6379 },
});
// Create a job
await queue.add("welcome", {
to: "[email protected]",
subject: "Welcome!",
body: "Thanks for signing up.",
});
Di dalamnya, BullMQ menjalankan Lua script di Redis yang secara atomic:
- Membuat hash berisi data job
- Mem-push ID job ke wait list
- Mem-publish event untuk worker yang sedang menunggu
Pengambilan job punya dua bagian. Worker menunggu di Redis sampai mungkin ada pekerjaan. Lalu script moveToActive mempromosikan delayed job yang udah eligible. Script ini secara atomic memindahkan job waiting atau prioritized berikutnya ke state active dan membuat lock-nya. Untuk BullMQ 5.81.3, script moveToActive menunjukkan urutan ini. Versi lain bisa mengubah detail command dan script-nya.
Lua script untuk atomicity: BullMQ memakai script Redis untuk mengoordinasikan beberapa perubahan state dalam satu operasi. Operasi ini mencakup pembuatan, aktivasi, penyelesaian, dan recovery job. Script-script ini nggak menyediakan transaction bersama database PostgreSQL yang terpisah.
Bagian dalam BullMQ
Deteksi stalled job: Worker memegang lock selama memproses job dan memperbaruinya secara berkala. Kalau lock-nya expire, stalled job checker milik BullMQ bisa memindahkan job itu kembali ke waiting supaya worker lain bisa memprosesnya. Panduan stalled jobs menyebutkan konsekuensi at least once-nya: sebuah job bisa jalan lagi setelah lock-nya hilang.
sequenceDiagram
participant W as Worker
participant R as Redis
participant SC as Stall Checker
W->>R: Move job to active state
W->>R: Set lock (configured duration)
loop Renew lock before expiry
W->>R: Renew lock
end
Note over W: Worker crashes!
SC->>R: Check active state
SC->>R: Lock expired?
SC->>R: Move job back to waiting
Rate limiting: BullMQ mendukung rate limiter di level queue. Panduan rate limiting dari BullMQ mencatat bahwa limit ini global untuk satu queue, jadi banyak worker berbagi batas yang sama.
Prioritas job: Job bisa punya prioritas dari 1 sampai 2,097,151, dengan angka yang lebih kecil didahulukan. Panduan prioritized jobs mendokumentasikan urutannya dan biaya sorted set-nya.
Flow dan dependency: BullMQ mendukung relasi job parent dan child. Parent bisa tetap di waiting-children sampai semua child-nya selesai, seperti yang dijelaskan di panduan flows.
Perbandingan arsitektur
| Aspek | PgBoss | BullMQ |
|---|---|---|
| Data store | PostgreSQL | Redis |
| Persistensi job | Pengaturan WAL dan transaction PostgreSQL | Redis RDB, AOF, keduanya, atau tanpa persistence, tergantung konfigurasi |
| Claim dan fetch | FOR UPDATE SKIP LOCKED di backend default | Worker wait ditambah Lua script moveToActive |
| Performa | Nggak di-benchmark di sini. Ukur perilaku PostgreSQL dan worker untuk workload target. | Nggak di-benchmark di sini. Ukur perilaku Redis dan worker untuk workload target. |
| Durability | Mengikuti pengaturan transaction dan WAL PostgreSQL. Lihat dokumentasi WAL. | Mengikuti policy persistence Redis yang dipilih. Lihat dokumentasi persistence Redis. |
| Infrastruktur tambahan | Nggak perlu queue store terpisah kalau PostgreSQL udah jadi bagian dari aplikasi | Butuh Redis |
| Atomicity | SQL transaction, termasuk transaction handle opsional untuk send() | Command Redis dan Lua script di dalam Redis, bukan transaction bersama PostgreSQL |
| Recovery | Expiration oleh supervisor dan pengecekan heartbeat opsional | Pembaruan lock dan recovery stalled-job, dengan pemrosesan at-least-once setelah lock hilang |
| Ordering | Job yang eligible dipilih berdasarkan prioritas dan urutan pembuatan; worker paralel bisa selesai nggak berurutan | FIFO mengatur urutan mulai secara default; worker paralel bisa selesai nggak berurutan, seperti yang dijelaskan panduan FIFO |
Repository ini nggak punya benchmark yang sebanding untuk kedua queue. Untuk membandingkannya, catat versi library dan database, hardware, topologi, dan ukuran payload. Tentukan concurrency enqueue dan worker, pengaturan batch, retry, dan pengaturan persistence. Dokumentasikan prosedur yang bisa diulang oleh orang lain.
Ukur enqueue rate dan completion rate, queue depth, CPU, dan pemakaian memori. Catat juga saturation, perilaku persistence, dan persentil latency untuk pickup dan penyelesaian job. Perbandingan di atas menjelaskan perilaku, tanpa klaim performa berupa angka.
Memilih queue sesuai workload
graph TD
A[Choose Your Job Queue] --> B{Already have Postgres?}
B -->|Yes| C{Need a measured performance target?}
C -->|No| D[PgBoss<br/>Reuse the database]
C -->|Yes| E{Can add Redis and benchmark it?}
E -->|Yes| F[BullMQ<br/>Compare measured results]
E -->|No| G[PgBoss<br/>Tune and measure PostgreSQL]
B -->|No| H{Already have Redis?}
H -->|Yes| F
H -->|No| I[Pick based on what<br/>you're willing to operate]
Pilih PgBoss kalau:
- Kamu udah punya PostgreSQL dan nggak mau menambah Redis
- Kamu butuh pembuatan job yang transactional: enqueue job cuma kalau transaction induknya ter-commit
- Kamu mau data queue diatur oleh policy transaction dan backup PostgreSQL
Pilih BullMQ kalau:
- Kamu udah punya Redis
- Kamu butuh fitur seperti prioritas job, flow, rate limiting, atau scheduling berbasis Redis
- Tim kamu udah nyaman mengoperasikan Redis
- Kamu punya benchmark workload yang membenarkan penambahan queue store
Enqueue secara transactional
PgBoss bisa meng-enqueue job di dalam database transaction yang sama dengan write bisnis. Dokumentasi transaction adapter resminya menunjukkan opsi db yang dipakai dengan transaction handle dari ORM:
import { fromKnex } from "pg-boss";
await knex.transaction(async (trx) => {
// Create the order in the application transaction
await trx("orders").insert({ id: orderId, status: "pending" });
// Enqueue the processing job in the same transaction
await boss.send(
"process-order",
{ orderId },
{ db: fromKnex(trx) },
);
});
// Both the order and the job commit or roll back together
Kalau transaction-nya di-rollback, insert job ikut ter-rollback. Queue API BullMQ menulis ke Redis dan nggak ikut dalam transaction PostgreSQL. Aplikasi yang harus mengoordinasikan kedua write ini biasanya butuh outbox atau metode koordinasi eksplisit lainnya.
Dengan outbox, aplikasi menulis event ke PostgreSQL di dalam transaction bisnis, lalu publisher terpisah mem-push event itu ke BullMQ. Ini memberi aplikasi titik recovery yang jelas, tapi desainnya beda dengan single transaction milik PgBoss.
Yang saya pelajari dari memakai PgBoss
Dari pengalaman memakai PgBoss di Xendit untuk produk NEX, ada beberapa pelajaran praktis:
- Pantau ukuran tabel job: job yang udah selesai tetap ada sampai pengaturan retention menghapusnya. Pakai
deleteAfterSecondsdan partitioning queue kalau cocok, lalu pantau ukuran tabel, index, dan perilaku vacuum. - Perhatikan connection pool: fetch dari worker, write penyelesaian, ukuran batch, dan listener
LISTEN/NOTIFYopsional semuanya memakai koneksi database. Tentukan ukuran pool berdasarkan concurrency worker dan aplikasi yang sebenarnya, bukan dengan asumsi satu koneksi per queue. - Lacak siklus supervisor: PgBoss meng-expire job, mengecek heartbeat, dan menjalankan scheduling dengan interval tertentu. Kalau instance atau database-nya overload, pengecekan itu bisa telat jalan. Dokumentasi worker dan opsi job menjelaskan pengaturan polling dan expiration.
- Rencanakan upgrade: permukaan API berubah antar versi, dan schema tabel internalnya juga berkembang. Saya menghabiskan banyak waktu memikirkan cara upgrade tanpa downtime. Strategi migrasinya penting.
- Kenali lock yang sedang kamu debug: claim job default memakai row lock PostgreSQL. Advisory lock dipakai untuk koordinasi schema, bukan sebagai leader maintenance yang terdokumentasi. Saya menulis post terpisah tentang advisory lock, tapi kedua jalur ini menyelesaikan masalah yang berbeda.
Cara saya memilih
PgBoss maupun BullMQ nggak ada yang lebih baik untuk semua kasus. PgBoss menyimpan state queue di PostgreSQL dan bisa ikut dalam transaction bisnis. BullMQ menyimpan state queue di Redis dan menyediakan prioritas, flow, rate limiting, dan scheduling berbasis Redis. Itu perbedaan arsitektur, bukan hasil benchmark.
Kalau Redis udah jadi bagian dari stack, BullMQ mungkin pilihan yang lebih simpel secara operasional untuk fitur-fitur itu. Kalau PostgreSQL adalah system of record dan enqueue harus ter-commit bersama write bisnis, PgBoss menghindari handoff antar store. Kalau throughput atau pickup latency jadi requirement, jalankan eksperimen khusus workload seperti yang dijelaskan di atas sebelum memilih.

Memuat komentar...