Message queue adalah antrean pesan di antara dua bagian sistem: yang satu menaruh pekerjaan, yang lain mengambil dan mengerjakannya nanti, tanpa saling menunggu. Bayangin dapur restoran. Kasir menempel struk pesanan di rel, lalu langsung melayani pelanggan berikutnya. Koki mengambil struk, memasak, lalu mencoret struknya. Kalau koki pingsan sebelum mencoret, struknya masih di rel, dan koki lain akan memasaknya lagi.
🧪 Tebakan cepat: worker TiketNada mengirim e-tiket pesanan #1141, lalu mati sebelum sempat bilang "selesai" ke queue. 10 detik kemudian, apa yang terjadi: pesannya hilang, e-tiketnya terkirim lagi, atau queue-nya berhenti? Tebak dulu, jawabannya ada di bagian "Ack, visibility timeout, dan at-least-once".
Producer, queue, consumer
- Producer: yang menaruh pesan ke queue. Di TiketNada, service
ordersetelah pembayaran sukses. - Message: isi pekerjaannya, biasanya JSON kecil:
{ "orderId": 1141 }. - Queue: tempat pesan menunggu sampai diambil.
- Consumer (sering disebut worker): yang mengambil pesan dan mengerjakannya. Di sini: membuat PDF e-tiket lalu mengirim email, 500 ms per pesanan.
- Ack (acknowledge): kabar dari consumer ke queue bahwa pesan selesai dikerjakan, jadi boleh dihapus. Ini "mencoret struk".
Yang masuk queue adalah pekerjaan yang boleh selesai belakangan. Saat user mendaftar, validasi format email dan menyimpan user ke database harus selesai di request itu juga, karena user perlu tahu hasilnya. Mengirim email selamat datang boleh lewat queue: user gak perlu menunggu emailnya terkirim untuk lanjut.
Kenapa pakai queue: decoupling dan menyerap lonjakan
Masalah utama yang diselesaikan queue adalah decoupling: producer gak perlu menunggu consumer selesai. Model contoh di Lab: jam 10.00, war tiket, 40 pembeli membayar bersamaan. API TiketNada punya 8 worker (proses yang melayani request).
| Tanpa queue | Dengan queue | |
|---|---|---|
| Request bayar | membuat e-tiket di dalam request: 5 + 500 ms | menaruh job di queue: 5 ms |
| 40 pembeli sekaligus | 5 putaran x 505 ms: pembeli ke-40 menunggu 2.525 ms | semua dijawab dalam 5 ms |
| E-tiket | jadi bersamaan dengan response | 2 worker x 2 per detik: habis dalam 10 detik |
| Pengiriman email gagal | pembeli melihat error 500 padahal sudah bayar | job dicoba lagi diam-diam |
| Semua worker mati | (gak ada worker terpisah) | API tetap 5 ms, job menumpuk aman di queue |
Lonjakan 40 pesanan "diserap" queue lalu dikerjakan dengan tempo yang sanggup ditangani worker. Kalau worker lambat atau mati sebentar, pesannya tetap aman menunggu.
Ack, visibility timeout, dan at-least-once
Saat worker mengambil pesan, queue gak langsung menghapusnya. Pesan itu cuma disembunyikan selama visibility timeout (10 detik di Lab; bawaan Amazon SQS 30 detik). Kalau worker mengirim ack sebelum waktunya habis, pesan dihapus. Kalau gak, queue menganggap worker-nya mati dan memunculkan pesan itu lagi untuk worker lain.
Ini yang terjadi di Lab saat worker mati setelah mengirim e-tiket tapi sebelum ack:
| Detik | Kejadian |
|---|---|
| 1 | worker mengambil #1141, mengirim e-tiket ke-1, lalu mati sebelum ack |
| 1 sampai 11 | #1141 tersembunyi, menunggu visibility timeout |
| 11 | timeout habis, #1141 muncul lagi, worker lain mengirim e-tiket ke-2 |
E-tiketnya terkirim lagi. Itu jawaban tebakannya. Pesan gak hilang, tapi bisa dikerjakan lebih dari sekali. Jaminan ini disebut at-least-once delivery: setiap pesan pasti dikerjakan, minimal sekali. Kebanyakan queue memberi jaminan ini, karena "tepat sekali" di jaringan yang bisa putus kapan saja sangat sulit.
Consumer harus idempoten
Karena pesan bisa datang dua kali, consumer harus idempoten: mengerjakan pesan yang sama dua kali hasilnya sama dengan sekali. Caranya: cek dulu apakah pekerjaan untuk orderId itu sudah pernah selesai.
const IDEMPOTENT = true;
const eTiket = new Map(); // orderId -> berapa e-tiket terkirim
function handle(job) {
if (IDEMPOTENT && eTiket.has(job.orderId)) {
console.log('#' + job.orderId + ' sudah punya e-tiket, lewati');
return; // tetap ack, tapi gak kirim lagi
}
kirimETiket(job); // bikin PDF, kirim email, catat di eTiket
}
eTiket.has(job.orderId)mengecek catatan pekerjaan yang sudah selesai. Di sistem nyata ini tabel database denganorder_idyang unique, bukanMapdi memori.- Kalau sudah ada, function berhenti tanpa mengirim apa-apa, lalu pesannya di-ack seperti biasa.
- Dengan
IDEMPOTENT = true, di detik 11 log-nya#1141 sudah punya e-tiket, lewati, dan pembeli tetap punya satu e-tiket.
Tanpa cek ini, pesan yang datang dua kali berarti dua e-tiket, dua email, atau lebih parah, dua kali potong saldo. Pola ini dibahas lebih dalam di lesson Idempotency Design.
Retry dan dead letter queue
Kalau pekerjaan gagal (exception), pesannya kembali ke queue dan dicoba lagi. Tapi ada pesan yang selalu gagal, misalnya pesanan #1142 dengan email tono@ yang gak valid. Pesan seperti ini disebut poison message. Kalau dicoba terus tanpa batas, dia memakan jatah worker selamanya.
Jadi retry diberi batas. Di Lab: dicoba lagi 2 detik setelah gagal, maksimal 3 kali (detik 1, 3, dan 5), lalu dipindah ke dead letter queue (DLQ): queue khusus untuk pesan yang gagal terus. Pesan lain tetap jalan, dan pesan di DLQ menunggu diperiksa manusia. Di Amazon SQS batasnya maxReceiveCount, di Laravel php artisan queue:work --tries=3 (gagalnya masuk tabel failed_jobs), di BullMQ opsi attempts.
Kafka, RabbitMQ, dan teman-temannya
- Point-to-point: satu pesan dikerjakan oleh satu worker. Cocok untuk task seperti kirim e-tiket.
- Pub/sub: satu pesan dikirim ke semua subscriber. Event
pesanan.dibayarbisa dipakai service email, statistik penjualan, dan notifikasi sekaligus.
| RabbitMQ | Kafka | |
|---|---|---|
| Bentuk | broker pesan | log event yang hanya ditambah di ujung |
| Setelah dikerjakan | pesan dihapus setelah ack | pesan tetap disimpan sesuai retention (misalnya 7 hari) |
| Replay | gak bisa | bisa, consumer membaca ulang dari posisi mana pun |
| Cocok untuk | task queue klasik, routing yang rumit | event streaming bervolume sangat tinggi |
Pilihan lain: Amazon SQS (dikelola cloud, gak perlu urus server), dan Redis dengan BullMQ (sederhana, cocok kalau kamu sudah punya Redis).
⚠️ Jebakan yang sering ketemu
- Consumer yang gak idempoten. At-least-once berarti duplikat pasti terjadi suatu saat. Cek dulu, baru kerjakan.
- Retry tanpa batas dan tanpa DLQ. Satu poison message bisa mengulang selamanya dan menyumbat worker.
- Visibility timeout lebih pendek dari waktu kerja. Kalau membuat PDF kadang 15 detik dan timeout-nya 10 detik, pesan yang sama diberikan ke worker kedua saat yang pertama masih bekerja.
- Gak memantau panjang queue. API terlihat sehat (tetap 5 ms) padahal semua worker mati dan ribuan e-tiket belum terkirim. Pantau panjang queue dan umur pesan tertua.
TL;DR: Message queue memisahkan producer dan consumer (decoupling): producer menaruh pesan lalu langsung lanjut, consumer mengerjakannya saat sanggup. Di model TiketNada, API menjawab 40 pembeli dalam 5 ms dan queue menyerap lonjakannya, bukan pembeli ke-40 menunggu 2.525 ms. Kerjakan lewat queue hanya yang boleh selesai belakangan, seperti email e-tiket atau email selamat datang. Pesan yang diambil disembunyikan selama visibility timeout; tanpa ack, pesan muncul lagi, jadi pengiriman at-least-once dan consumer wajib idempoten. Pesan yang gagal dicoba ulang dengan batas, lalu dipindah ke dead letter queue. Point-to-point untuk task, pub/sub untuk event. RabbitMQ menghapus pesan setelah ack dan kuat di routing; Kafka menyimpan log event yang bisa di-replay dengan throughput sangat tinggi.