Kapan Pesan Menjadi "Dead Letter"
Sebuah pesan dikirim ke dead letter exchange bila salah satu dari tiga hal terjadi:
1. Ditolak dengan requeue=false
nack(msg, false, false) atau reject(msg, false)
2. Kedaluwarsa (TTL habis)
dari x-message-ttl pada queue, atau expiration pada pesan
3. Queue penuh (overflow) dengan kebijakan reject-publish-dlx
atau pesan terdorong keluar oleh drop-head
Tambahan pada quorum queue:
4. Melewati x-delivery-limit → otomatis di-dead-letterMenyiapkan DLX
RA="rabbitmqadmin -u admin -p rahasia"
# tempat pembuangan akhir
$RA declare exchange name=pesanan.dlx type=direct durable=true
$RA declare queue name=pesanan.email.dlq durable=true
$RA declare binding source=pesanan.dlx destination=pesanan.email.dlq \
routing_key=pesanan.email
# queue kerja yang menunjuk ke DLX
$RA declare queue name=pesanan.email durable=true arguments='{
"x-queue-type": "quorum",
"x-dead-letter-exchange": "pesanan.dlx",
"x-dead-letter-routing-key": "pesanan.email",
"x-delivery-limit": 5
}'x-dead-letter-routing-key, routing key asli dipertahankanBila tidak diset, pesan masuk ke DLX dengan routing key aslinya — misalnya pesanan.jakarta.dibuat. Bila DLX Anda bertipe direct dan bindingnya bernama pesanan.email, tidak ada yang cocok dan pesan hilang untuk kedua kalinya.
Selalu setel routing key DLX secara eksplisit, atau gunakan DLX bertipe fanout agar apa pun routing key-nya tetap tertangkap.
Header yang Ditambahkan Broker
Saat sebuah pesan di-dead-letter, RabbitMQ menyisipkan header x-death yang berisi riwayat lengkapnya — informasi paling berharga saat menyelidiki isi DLQ.
{
"x-death": [
{
"count": 3,
"reason": "rejected",
"queue": "pesanan.email",
"exchange": "pesanan",
"routing-keys": ["pesanan.jakarta.dibuat"],
"time": "2026-07-28T09:14:22Z"
}
],
"x-first-death-reason": "rejected",
"x-first-death-queue": "pesanan.email",
"x-first-death-exchange": "pesanan"
}reason bernilai rejected, expired, maxlen, atau delivery_limit. Ini langsung memberi tahu Anda kenapa pesan berakhir di sini.
Pola Retry dengan Backoff Berjenjang
Ini pola paling berguna dan paling sering salah diterapkan. Idenya: kegagalan sementara diberi kesempatan lagi setelah jeda yang makin panjang, tetapi jumlah percobaannya terbatas.
gagal (nack requeue=false)
pesanan.email ─────────────────────────> pesanan.dlx
▲ │ rk = retry.5s
│ ▼
│ ┌──────────────────────┐
│ TTL habis, dead-letter │ retry.5s │
└──────────────────────────────│ x-message-ttl: 5000 │
│ │ DLX → pesanan (asal) │
│ └──────────────────────┘
│ │ gagal lagi
│ ▼ rk = retry.30s
│ ┌──────────────────────┐
└──────────────────────────────│ retry.30s (30 detik) │
└──────────────────────┘
│ gagal lagi
▼ rk = retry.5m
┌──────────────────────┐
│ retry.5m (5 menit) │
└──────────────────────┘
│ masih gagal
▼
┌──────────────────────┐
│ pesanan.email.dlq │ manusia
│ (tanpa TTL, mati) │ memeriksa
└──────────────────────┘Queue retry.5s tidak dikonsumsi siapa pun. Pesan hanya duduk di sana sampai TTL-nya habis, lalu broker sendiri yang memindahkannya kembali ke exchange asal lewat DLX. Penundaan dikerjakan broker, bukan oleh sleep di konsumer Anda — yang akan memblokir worker dan memboroskan koneksi.
RA="rabbitmqadmin -u admin -p rahasia"
for tingkat in "5s:5000" "30s:30000" "5m:300000"; do
nama="${tingkat%%:*}"; ttl="${tingkat##*:}"
$RA declare queue name="retry.$nama" durable=true arguments="{
\"x-message-ttl\": $ttl,
\"x-dead-letter-exchange\": \"pesanan\",
\"x-dead-letter-routing-key\": \"pesanan.jakarta.dibuat\"
}"
$RA declare binding source=pesanan.dlx destination="retry.$nama" \
routing_key="retry.$nama"
done
# DLQ terakhir: tanpa TTL, tanpa DLX — pesan berhenti di sini
$RA declare queue name=pesanan.email.dlq durable=true
$RA declare binding source=pesanan.dlx destination=pesanan.email.dlq \
routing_key=habisconst TINGKAT = ['retry.5s', 'retry.30s', 'retry.5m'];
function percobaanKe(msg) {
const d = (msg.properties.headers || {})['x-death'];
if (!Array.isArray(d) || !d.length) return 0;
return d.reduce((a, x) => a + (x.count || 0), 0);
}
await ch.consume('pesanan.email', async (msg) => {
if (!msg) return;
try {
await proses(JSON.parse(msg.content.toString()));
ch.ack(msg);
} catch (e) {
if (e.permanen) { // payload rusak, skema salah
ch.publish('pesanan.dlx', 'habis', msg.content, msg.properties);
ch.ack(msg);
return;
}
const n = percobaanKe(msg);
const tujuan = n < TINGKAT.length ? TINGKAT[n] : 'habis';
ch.publish('pesanan.dlx', tujuan, msg.content, msg.properties);
ch.ack(msg); // ack pesan lama, salinan sudah pindah
}
}, { noAck: false });RabbitMQ mengevaluasi kedaluwarsa dari depan antrean. Bila pesan pertama punya TTL 10 menit dan pesan kedua TTL 5 detik, pesan kedua tidak akan kedaluwarsa lebih dulu — ia menunggu giliran di belakang.
Karena itu pola retry di atas memakai queue terpisah per tingkat penundaan, bukan satu queue dengan TTL berbeda per pesan. Semua pesan dalam satu queue retry punya TTL yang sama, sehingga urutannya konsisten.
DLQ Harus Bisa Ditindaklanjuti
DLQ yang tidak pernah dilihat siapa pun sama saja dengan membuang pesan, hanya dengan langkah tambahan. Yang membuatnya berguna:
- Alert bila jumlah pesan di DLQ melebihi nol untuk jangka waktu tertentu.
- Pemilik yang jelas — nama queue menyebut tim yang bertanggung jawab.
- Alat pemutar ulang untuk mengembalikan pesan setelah bug diperbaiki.
- Retensi — TTL panjang (misalnya 7 hari), agar DLQ sendiri tidak menjadi sumber alarm disk.
rabbitmqadmin -u admin -p rahasia \
move_messages src_queue=pesanan.email.dlq dest_queue=pesanan.email
# atau lewat Shovel sementara, yang bisa dipantau dan dihentikan:
rabbitmqctl set_parameter shovel putar-ulang '{
"src-protocol": "amqp091",
"src-uri": "amqp://localhost",
"src-queue": "pesanan.email.dlq",
"dest-protocol": "amqp091",
"dest-uri": "amqp://localhost",
"dest-queue": "pesanan.email",
"src-delete-after": "queue-length"
}'Perhatikan src-delete-after: queue-length — shovel berhenti sendiri setelah memindahkan pesan yang ada saat ia dimulai, sehingga tidak berputar tanpa henti bila pesan gagal lagi.
Penundaan Sungguhan: Delayed Message Exchange
Untuk penundaan yang presisi dan bervariasi per pesan — misalnya "kirim pengingat 24 jam sebelum jatuh tempo" — plugin rabbitmq_delayed_message_exchange lebih tepat daripada rantai TTL.
await ch.assertExchange('penjadwal', 'x-delayed-message', {
durable: true,
arguments: { 'x-delayed-type': 'topic' }
});
ch.publish('penjadwal', 'pengingat.jatuh-tempo', body, {
persistent: true,
headers: { 'x-delay': 86400000 } // tunda 24 jam
});Catatan penting: plugin ini menyimpan pesan tertunda di basis data internal node, bukan di queue. Ia tidak direplikasi di cluster — bila node itu hilang, pesan tertunda ikut hilang. Untuk penjadwalan jangka panjang yang kritis, tabel di basis data ditambah scheduler tetap lebih aman.