Melepas beban dari request dengan pemrosesan asinkron: mengenal message queue (RabbitMQ, Kafka, SQS), membangun worker, pola reliable delivery, dan menerapkannya pada notifikasi order dan pembayaran

Di episode 9 kita mempercepat request yang sinkron. Tapi ada jenis pekerjaan yang tidak boleh dilakukan di dalam request sama sekali: mengirim email, memproses pembayaran, meng-generate laporan, atau memanggil API eksternal yang lambat. Jika semua ini menunggu dalam satu request, response jadi lambat — dan jika layanan eksternal mati, request ikut gagal.
Solusinya adalah pemrosesan asinkron dengan message queue: request hanya mencatat pekerjaan ("kirim email untuk order 87"), lalu langsung merespons. Pekerjaan dijalankan di worker terpisah. Episode ini membangun konsep queue, mengenal RabbitMQ, Kafka, dan SQS, serta menerapkannya pada notifikasi order dan pembayaran.
Bayangkan alur checkout tanpa queue: buat order → kirim email → panggil payment → update stok, semua di dalam satu request. Jika email service lambat 3 detik, user menunggu 3 detik; jika mati, checkout gagal total.
Request utama selesai dalam milidetik; pekerjaan berat berjalan paralel di worker. Keuntungan lain: worker bisa diskalakan terpisah, dan pesan yang gagal bisa dicoba lagi.
Pahami lima istilah inti:
RabbitMQ adalah message broker yang paling umum untuk pola work queue — satu pesan dikonsumsi satu worker. Cocok untuk notifikasi, email, dan tugas-tugas yang dikerjakan sekali.
docker run -d --name rabbit-lab -p 5672:5672 -p 15672:15672 rabbitmq:3-managementimport amqp from "amqplib"
const conn = await amqp.connect(process.env.RABBITMQ_URL!)
const channel = await conn.createChannel()
await channel.assertQueue("notifications", { durable: true })
router.post("/v1/orders", async (req, res) => {
const order = await createOrder(req.body)
channel.sendToQueue("notifications", Buffer.from(JSON.stringify({
type: "order.created",
orderId: order.id,
email: req.body.email,
})), { persistent: true })
res.status(201).json({ data: order })
})import amqp from "amqplib"
const conn = await amqp.connect(process.env.RABBITMQ_URL!)
const channel = await conn.createChannel()
await channel.assertQueue("notifications", { durable: true })
channel.prefetch(1)
channel.consume("notifications", async (msg) => {
const { type, orderId, email } = JSON.parse(msg.content.toString())
try {
await sendEmail(email, type)
channel.ack(msg)
} catch (err) {
// gagal sementara → jangan ack, broker mengirim ulang
channel.nack(msg, false, true)
}
})prefetch(1) membuat worker mengambil satu pesan pada satu waktu — mencegah satu worker menumpuk pesan yang belum selesai. ack/nack adalah mekanisme kepercayaan antara worker dan broker.
Note
durable: true dan persistent: true menjamin pesan tidak hilang saat broker restart. Pola yang wajib untuk notifikasi pembayaran — pesan yang hilang berarti pelanggan tidak diberi tahu ordernya.
Kafka berbeda dari RabbitMQ: alih-alih antrean yang habis setelah dikonsumsi, ia adalah log yang bisa dibaca berulang (event streaming). Satu pesan bisa dikonsumsi banyak consumer group — cocok untuk audit trail, analytics, dan event sourcing (episode 22).
import { Kafka } from "kafkajs"
const kafka = new Kafka({ brokers: ["localhost:9092"] })
const producer = kafka.producer()
async function publishEvent(topic: string, payload: object) {
await producer.connect()
await producer.send({
topic,
messages: [{ value: JSON.stringify(payload) }],
})
}
await publishEvent("order-events", { orderId: 87, action: "created" })Consumer Kafka melacak posisi bacanya (offset) per consumer group:
const consumer = kafka.consumer({ groupId: "email-workers" })
await consumer.connect()
await consumer.subscribe({ topic: "order-events", fromBeginning: false })
await consumer.run({
eachMessage: async ({ message }) => {
const event = JSON.parse(message.value.toString())
if (event.action === "created") await sendEmail(event.orderId)
},
})RabbitMQ vs Kafka — keputusan singkat: RabbitMQ untuk task distribution (email, upload, satu pekerjaan sekali); Kafka untuk event streaming (audit, analytics, re-read, banyak consumer).
Jika aplikasi sudah di AWS, SQS adalah managed queue — tanpa perlu mengoperasikan broker sendiri. Keunggulan: skalabilitas otomatis, tidak perlu maintenance. Pola kerjanya sama: SendMessage → worker ReceiveMessage + DeleteMessage:
API → SQS.sendMessage(OrderCreated)
Worker → SQS.receiveMessage → proses → SQS.deleteMessageAturan delivery: SQS at-least-once — pesan bisa dikirim lebih dari sekali, jadi worker harus idempoten (proses dua kali = hasil sama).
Tiga pola yang membuat pipeline async andal:
Worker harus aman dipanggil berkali-kali. Contoh: menyimpan email "sudah dikirim" sebelum benar-benar kirim, atau memakai orderId sebagai kunci unik — sehingga pengiriman ganda menghasilkan satu email.
Pesan yang gagal terus-menerus tidak boleh memblokir queue. Pindahkan ke DLQ setelah beberapa percobaan, lalu dianalisis manual:
channel.consume("notifications", async (msg) => {
try {
await sendEmail(...)
channel.ack(msg)
} catch (err) {
const attempts = (msg.properties.headers?.attempts ?? 0) + 1
if (attempts >= 3) {
await channel.sendToQueue("notifications-dlq", msg.content, { persistent: true })
channel.ack(msg)
} else {
channel.nack(msg, false, false)
}
}
})Retry langsung menyemprot broker. Beri jeda bertingkat (exponential backoff): 1s, 5s, 30s — atau pakai delayed queue.
Warning
Pesan yang di-nack tanpa batas akan berputar selamanya dan memakan CPU. Selalu batasi percobaan dan arahkan kegagalan persisten ke DLQ — kalau tidak, satu pesan rusak bisa melumpuhkan seluruh queue.
Terapkan alur lengkap untuk toko online kita:
POST /v1/orders menyimpan order lalu publish order.created.email-worker mengirim konfirmasi ke user.inventory-worker mengurangi stok.docker exec rabbit-lab rabbitmqctl list_queues name messagesmessages menunjukkan jumlah antrean — bertambah saat order masuk, berkurang saat worker selesai.
Queue tidak mempercepat jika API tetap menunggu hasilnya sinkron. Kalau butuh hasil segera, itu bukan pekerjaan untuk queue — itu pekerjaan untuk pola lain (misal poll status).
Pesan yang dikirim ulang (karena timeout ack) diproses dua kali → email ganda, pembayaran ganda. Idempotency adalah prasyarat, bukan bonus.
Pesan menumpuk tanpa disadari. Monitoring panjang queue harus jadi alarm pertama (episode 11).
Episode 10 melepas pekerjaan berat dari request: konsep producer-queue-consumer, RabbitMQ untuk task distribution, Kafka untuk event log, SQS sebagai managed queue, plus pola reliable delivery — idempotency, DLQ, dan retry backoff.
Inti yang harus dibawa pulang:
durable + persistent + ack adalah dasar pesan tidak hilang.Di episode 11 selanjutnya kita akan membuat sistem terlihat: logging & error handling — structured logging, taksonomi error, dan tracing dasar agar produksi tidak buta. Sampai jumpa di episode 11!