Kembali ke jurnalCATATAN FAJAR
Software Engineering9 menit baca

PgBoss vs BullMQ: Arsitektur Job Queue di Balik Layar

Membandingkan cara PgBoss dan BullMQ menyimpan, mengambil, menyelesaikan, dan memulihkan background job.

Di artikel ini 13 bagian

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

mermaid
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:

typescript
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:

sql
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.

mermaid
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.

typescript
// 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

mermaid
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:

typescript
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:

  1. Membuat hash berisi data job
  2. Mem-push ID job ke wait list
  3. 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.

mermaid
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

AspekPgBossBullMQ
Data storePostgreSQLRedis
Persistensi jobPengaturan WAL dan transaction PostgreSQLRedis RDB, AOF, keduanya, atau tanpa persistence, tergantung konfigurasi
Claim dan fetchFOR UPDATE SKIP LOCKED di backend defaultWorker wait ditambah Lua script moveToActive
PerformaNggak 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.
DurabilityMengikuti pengaturan transaction dan WAL PostgreSQL. Lihat dokumentasi WAL.Mengikuti policy persistence Redis yang dipilih. Lihat dokumentasi persistence Redis.
Infrastruktur tambahanNggak perlu queue store terpisah kalau PostgreSQL udah jadi bagian dari aplikasiButuh Redis
AtomicitySQL transaction, termasuk transaction handle opsional untuk send()Command Redis dan Lua script di dalam Redis, bukan transaction bersama PostgreSQL
RecoveryExpiration oleh supervisor dan pengecekan heartbeat opsionalPembaruan lock dan recovery stalled-job, dengan pemrosesan at-least-once setelah lock hilang
OrderingJob yang eligible dipilih berdasarkan prioritas dan urutan pembuatan; worker paralel bisa selesai nggak berurutanFIFO 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

mermaid
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:

typescript
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 deleteAfterSeconds dan 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/NOTIFY opsional 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.

TOPIK

MAKASIH UDAH BACA

Gimana menurutmu?

Reaksi atau obrolan, dua-duanya selalu ditunggu.

Memuat reaksi…

Bagikan

Memuat komentar...

LANJUT JELAJAH

Satu pikiran bawa ke pikiran lain.

Semua tulisan
Kembali ke semua tulisanSatu catatan, pelan-pelan.