Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Apa perbedaan pub/sub dan message queue?
Message queue mengirim setiap message ke tepat satu consumer dari sebuah pool, jadi fungsinya membagi pekerjaan. Pub/sub mengirim setiap message ke semua subscriber, jadi fungsinya menyiarkan event ke banyak pembaca yang independen. Queue menjawab siapa yang mengerjakan, pub/sub menjawab siapa yang perlu tahu.
02Apakah Redis Pub/Sub itu message queue?
Bukan. Dokumentasi Redis menggambarkan Pub/Sub sebagai at-most-once delivery: jika subscriber terputus atau gagal, message hilang selamanya. Ia juga tidak menyimpan apa pun. Untuk perilaku seperti queue, pakai library job seperti BullMQ, atau Redis Streams jika butuh message yang tersimpan.
03Bisakah message queue melakukan pub/sub?
Beberapa broker bisa, dengan memberi setiap subscriber queue sendiri yang terikat ke exchange atau topic yang sama. Sistem berbasis log seperti Kafka melakukannya dengan consumer group: instance dalam satu group berbagi message seperti queue, dan setiap group tambahan menerima semuanya. Jadi kedua model tidak sepisah yang terlihat.
04Kapan sebaiknya memakai pub/sub dibanding queue?
Pakai pub/sub saat satu event harus sampai ke beberapa service independen, misalnya order selesai memicu loyalty, analytics dan dashboard. Pakai queue saat tepat satu worker harus mengerjakan tugas, misalnya mengirim struk. Jika subscriber tidak boleh ketinggalan event, pilih log yang tahan lama, bukan pub/sub transient.
05Apa itu dead-letter queue dan apakah saya membutuhkannya?
Dead-letter queue menyimpan message yang gagal di semua retry supaya bisa diperiksa dan tidak memblokir atau berputar selamanya. Anda memerlukan jalur dead-letter kapan pun message bisa menjadi poison, misalnya payload yang rusak. Sebagian broker menyediakannya, sedangkan NATS JetStream menerbitkan advisory max-deliveries dan Anda memarkir message sendiri.
Pub/Sub vs Message Queue: Perbedaan dan Kapan Memakainya
Pub/sub vs message queue: competing consumers versus fan-out, delivery guarantee, ordering, replay, consumer group, plus checklist untuk job, notifikasi dan event.
Message queue memberikan setiap message ke satu consumer, jadi para worker berbagi backlog job. Pub/sub mengirim setiap message ke semua subscriber, jadi banyak service bereaksi pada satu event. Queue punya acknowledgement dan retry, pub/sub transient tidak punya keduanya, dan broker berbasis log seperti Kafka dan NATS JetStream memakai consumer group untuk menawarkan keduanya.
Pertanyaan ini biasanya muncul sebagai komentar di design review: ini sebaiknya queue atau pub/sub? Di ERP POS atau carwash, satu event yang sama, yaitu cuci selesai, harus menghasilkan struk WhatsApp, poin loyalty, display board yang ter-refresh, dan satu baris di laporan. Pekerjaan-pekerjaan itu tidak sejenis, dan broker yang cocok untuk satu bisa salah untuk yang lain.
Post ini adalah panduan keputusan secara konseptual, bukan tutorial tool. Isinya membandingkan kedua model, menunjukkan snippet paling kecil yang nyata untuk BullMQ, Redis Pub/Sub dan consumer group Kafka, menurunkan biaya tiap failure mode dengan hitungan, lalu ditutup dengan checklist. Setiap perilaku yang disebut diambil dari dokumentasi resmi di bagian akhir.
Apa perbedaan inti antara pub/sub dan message queue?
Perbedaannya ada pada siapa yang menerima message. Di queue, sekumpulan consumer membaca dari backlog yang sama dan setiap message masuk ke tepat satu consumer, yaitu pengiriman point-to-point dengan competing consumers. Menambah worker membuat backlog habis lebih cepat. Di pub/sub, publisher tidak tahu siapa yang mendengarkan: dokumentasi Redis menggambarkan publisher yang tidak diprogram untuk mengirim ke penerima tertentu, dan subscriber yang menerima apa yang mereka minta tanpa tahu siapa publisher-nya. Menambah subscriber berarti menambah pembaca stream yang sama, bukan menambah throughput.
Jadi keduanya menjawab pertanyaan yang berbeda. Queue menjawab: siapa yang akan mengerjakan tugas ini? Pub/sub menjawab: siapa yang perlu tahu bahwa ini terjadi? Pekerjaan yang harus terjadi sekali, seperti menagih kartu atau mengirim struk, cocok di queue. Fakta yang direspons beberapa service yang tidak saling terkait, seperti cuci selesai, cocok di pub/sub.
Bagaimana message queue dengan competing consumers bekerja?
Producer menaruh job di queue bernama dan worker mana pun yang terhubung ke queue itu boleh mengambilnya. Di BullMQ, producer memanggil add pada Queue, dan worker dibuat dengan nama queue yang sama. Menjalankan worker di dua proses memberi Anda dua competing consumers, dan setiap job hanya sampai ke salah satunya.
import { Queue, Worker } from "bullmq";
const connection = { host: "127.0.0.1", port: 6379 };
// Producer: one job per receipt. attempts + backoff are the retry policy.
const receipts = new Queue("receipts", { connection });
await receipts.add(
"send-receipt",
{ washId: 8841, channel: "whatsapp" },
{ attempts: 3, backoff: { type: "exponential", delay: 1000 } },
);
// Consumer: start this in two processes with the SAME queue name.
// Each job is handed to exactly one of them (competing consumers).
const worker = new Worker(
"receipts",
async (job) => {
await sendReceipt(job.data); // throw = failed attempt, BullMQ retries it
},
{ connection },
);
// After the last attempt the job lands in the failed set. Look at it, alert on it.
worker.on("failed", (job, err) => alertOps(job?.id, err.message));
Bagian yang penting adalah apa yang terjadi saat worker gagal. BullMQ menerima retry policy sebagai opsi job: attempts menentukan total percobaan dan backoff menentukan delay fixed atau exponential dalam milidetik. Ketika worker tidak bisa memperbarui lock pada sebuah job, job ditandai stalled dan dikembalikan ke waiting supaya worker lain memprosesnya lagi. Itulah janji queue dalam satu kalimat: job tidak terlupakan karena satu worker mati, dan harganya adalah job bisa berjalan lebih dari sekali.
Bagaimana fan-out pub/sub bekerja, dan apa yang tidak diingatnya?
Subscriber menyatakan minat pada sebuah channel dan broker mendorong setiap message di channel itu ke setiap subscriber yang sedang terhubung. Di Redis itu adalah SUBSCRIBE dan PUBLISH. Dokumentasi mencatat bahwa subscriber menerima message sesuai urutan publish, dan bahwa client yang subscribe di RESP2 hanya boleh mengirim command subscription, jadi aplikasi nyata memakai koneksi terpisah untuk publish.
import Redis from "ioredis";
// A connection in subscribed mode cannot run normal commands (RESP2),
// so the subscriber and the publisher are two separate connections.
const sub = new Redis();
const pub = new Redis();
// Every process that runs this gets EVERY message. No group, no queue.
await sub.subscribe("wash.completed");
sub.on("message", (channel, payload) => {
refreshDisplayBoard(JSON.parse(payload));
});
// PUBLISH returns how many subscribers it reached. 0 means the message is gone.
const reached = await pub.publish("wash.completed", JSON.stringify({ washId: 8841 }));
Yang tidak dilakukan Redis Pub/Sub adalah mengingat. Dokumentasi menyebut semantik at-most-once: message dikirim sekali kalau memang sampai, dan jika subscriber mengalami error atau network disconnect, message hilang selamanya. Tidak ada yang disimpan untuk subscriber yang sedang tidak terhubung. Untuk message yang tersimpan, halaman yang sama menunjuk ke Redis Streams, yang menyimpan message dan mendukung at-most-once maupun at-least-once.
Contoh hitungan: jika subscriber restart selama 10 detik sementara channel membawa 2 message per detik, ia tidak pernah melihat 10 x 2 = 20 message, dan tidak ada yang melaporkannya. Itu aman untuk display board yang digambar ulang di event berikutnya, dan menjadi bug diam-diam untuk apa pun yang harus dihitung.
Delivery, ordering, retention: bagaimana queue dan pub/sub dibandingkan?
Tiga properti menentukan sebagian besar desain: apa yang terjadi saat consumer mati, apakah urutan dijaga, dan apakah Anda bisa membaca message lagi. Tabel membandingkan job queue klasik, pub/sub transient (Redis Pub/Sub, dan core NATS yang juga at-most-once), dan broker berbasis log dengan consumer group (Kafka, NATS JetStream).
Pertanyaan
Job queue (BullMQ)
Pub/sub transient (Redis Pub/Sub)
Log dengan consumer group (Kafka)
Siapa yang menerima message
Satu worker dari pool
Setiap subscriber yang terhubung
Satu consumer per group, semua group
Delivery guarantee
Job yang stalled atau gagal berjalan lagi, jadi efeknya at-least-once
At-most-once: hilang jika subscriber mati atau error
At-least-once jika offset di-commit setelah diproses
Consumer offline
Job menunggu di queue sampai worker kembali
Message yang dipublish selama itu hilang
Record menunggu di log, group melanjutkan dari committed offset
Replay message lama
Tidak, job yang selesai sudah dikonsumsi
Tidak, tidak ada yang disimpan
Ya, consumer bisa menentukan offset yang lebih awal
Ordering
Diambil kira-kira berurutan, tetapi urutan selesai tidak dijamin dengan banyak worker atau retry
Dikirim sesuai urutan publish
Terjaga dalam satu partition, tidak lintas partition
Penanganan failure
attempts, backoff dan failed set
Tidak ada, itu urusan subscriber
Baca ulang dari committed offset terakhir, atau nak dan redelivery AckWait di JetStream
Baca tabel per baris, bukan per kolom. Kalau baris Consumer offline harus berisi tidak ada yang hilang, pub/sub transient gugur. Kalau Anda butuh Replay message lama, job queue klasik gugur. Sel ordering adalah yang sering terlewat: dengan banyak worker dan retry, job selesai dalam urutan berbeda dari saat dimulai, jadi desain yang butuh urutan ketat harus mem-partition berdasarkan key.
Bagaimana consumer group menggabungkan queue dan pub/sub di Kafka dan NATS?
Broker berbasis log menambahkan message ke log yang tahan lama dan menyimpan posisi, disebut offset, untuk setiap consumer group. Dokumentasi desain Confluent menyatakan setiap partition dikonsumsi oleh tepat satu consumer dalam setiap consumer group pada satu waktu. Taruh beberapa instance dalam satu group dan mereka berbagi partition seperti worker queue yang bersaing. Buat group kedua dan ia menerima record yang sama secara independen, itulah fan-out. Satu mekanisme, dua model.
import { Kafka } from "kafkajs";
const kafka = new Kafka({ clientId: "qilap-api", brokers: ["localhost:9092"] });
// Same topic, two groups. Within a group the partitions are shared out
// (queue behaviour); across groups every group sees everything (pub/sub).
const loyalty = kafka.consumer({ groupId: "loyalty" });
const analytics = kafka.consumer({ groupId: "analytics" });
await loyalty.connect();
await loyalty.subscribe({ topics: ["wash.completed"], fromBeginning: true });
await loyalty.run({
eachMessage: async ({ topic, partition, message }) => {
await addPoints(JSON.parse(message.value!.toString()));
// offset is committed after this resolves: a crash re-delivers (at-least-once)
},
});
// analytics.run(...) is identical with its own groupId, and its own offsets.
Biayanya adalah broker menyimpan semuanya sampai retention menghapusnya, dan group yang crash mulai lagi dari committed offset terakhir, jadi bisa memproses ulang record yang sudah ditangani. Confluent mendokumentasikan hal itu: setelah reassignment, posisi awal adalah committed offset terakhir. Consumer juga bisa menentukan offset yang lebih awal untuk mengonsumsi ulang data, yaitu baris replay di tabel. Opsi KafkaJS fromBeginning: true adalah cara group baru meminta seluruh riwayat.
Subscriber baru pada topic berbasis log bisa melakukan backfill riwayat dengan mulai dari offset yang lebih awal, hal yang sama sekali tidak bisa dilakukan channel pub/sub transient. Kalau service di masa depan mungkin butuh event kemarin, itu saja sudah bisa membenarkan pemakaian log.
Apa yang terjadi saat consumer gagal: acknowledgement, redelivery dan dead letter?
Acknowledgement adalah consumer yang memberi tahu broker bahwa pekerjaan sudah selesai. Tanpa itu broker harus menganggapnya gagal dan mengirim ulang. NATS JetStream mendokumentasikan empat respons: ack untuk berhasil, nak untuk kirim ulang, term untuk berhenti mencoba, dan in-progress untuk mereset timer AckWait. Nak biasa mengirim ulang seketika, yang bisa menjadi retry loop yang rapat, jadi beri delay seperti di snippet.
// JetStream pull consumer named "shipping", explicit acks.
const c = await js.consumers.get("ORDERS", "shipping");
const messages = await c.consume();
for await (const m of messages) {
try {
await handle(m);
m.ack(); // work done: server will not redeliver
} catch {
m.nak(10_000); // redeliver after 10 s instead of in a tight loop
}
}
Contoh hitungan dengan default yang terdokumentasi: AckWait default 30 detik dan MaxDeliver default -1, artinya tanpa batas. Poison message yang handler-nya menggantung dikirim ulang setiap 30 detik selamanya. Mengatur MaxDeliver ke 5 membatasinya pada 5 x 30 = 150 detik menunggu sebelum server berhenti mengirimnya.
Berhenti tidak sama dengan menangani. JetStream tidak punya dead-letter queue bawaan, dan menerbitkan advisory max-deliveries sebagai gantinya, jadi Anda subscribe ke advisory itu dan memarkir message sendiri. BullMQ menyimpan job di failed set setelah attempt terakhir, tergantung pengaturan removal Anda. Bagaimanapun, jalur dead-letter harus Anda pasang lalu pantau, kalau tidak failure menumpuk tanpa terlihat.
Mana yang dipakai untuk notifikasi, background job dan event?
Petakan setiap pekerjaan ke model yang sesuai kebutuhannya. Pemetaan di bawah berasal dari contoh ERP carwash, di mana satu penyelesaian cuci memicu beberapa reaksi yang tidak saling terkait.
Notifikasi seperti struk WhatsApp atau email: queue. Harus terjadi sekali, retry saat gagal, dan bertahan saat worker restart.
Background job seperti membuat laporan atau resize gambar: queue. Anda butuh backlog, tambahan worker saat backlog membesar, dan failed set untuk diperiksa.
Event seperti cuci selesai: pub/sub, dan log dengan consumer group jika ada subscriber yang tidak boleh ketinggalan atau service baru mungkin butuh riwayat. Redis Pub/Sub biasa untuk sinyal live yang boleh hilang, seperti me-refresh layar.
Kalau ragu, ajukan empat pertanyaan ini berurutan. Jawaban pertama yang menentukan biasanya menentukan seluruh desain.
Apakah ini pekerjaan yang harus dikerjakan tepat satu worker? Pakai queue.
Apakah beberapa service independen masing-masing butuh setiap event? Pakai pub/sub, dengan satu consumer group atau subscription per service.
Apakah kehilangan message saat consumer mati tidak bisa diterima? Coret pub/sub transient dan pilih queue atau log.
Apakah consumer di masa depan mungkin butuh event lama? Pilih broker berbasis log dengan retention.
Pilih berdasarkan apa yang terjadi pada message setelah dipublish. Kalau satu worker harus mengerjakannya, pakai queue. Kalau banyak service harus mendengarnya, pakai pub/sub. Kalau banyak service harus mendengarnya dengan andal dan pendatang baru mungkin butuh masa lalu, pakai log dengan consumer group. Apa pun pilihannya, buat handler idempotent, karena retry dan redelivery berarti satu message bisa tiba dua kali.