Retry berguna ketika kegagalan bersifat sementara. Database mungkin tidak tersedia selama beberapa detik, service downstream dapat mengembalikan respons overload, atau koneksi jaringan dapat terputus lalu pulih.

Retry menjadi merugikan ketika pesan itu sendiri memang tidak mungkin berhasil.

Payload yang malformed, versi schema yang tidak didukung, referensi ke data yang hilang secara permanen, atau bug aplikasi yang deterministik dapat membuat pesan yang sama gagal pada setiap delivery. Jika broker terus mengembalikan pesan itu kepada consumer tanpa batas, sistem menghabiskan kapasitas untuk mengulang pekerjaan yang tidak punya peluang berhasil.

Pesan seperti ini sering disebut poison message.

Dead-letter queue (DLQ) memberi sistem tempat untuk mengarantina pesan yang seharusnya berhenti mengikuti loop retry normal. DLQ bukan solusi akhir dengan sendirinya. Desain yang andal juga menentukan kegagalan mana yang layak di-retry, berapa banyak percobaan yang berguna, konteks diagnostik apa yang perlu dipertahankan, bagaimana operator menemukan kegagalan, dan bagaimana pesan dikembalikan ke produksi setelah masalah dasarnya diperbaiki.

Mulai dengan memisahkan kegagalan sementara dan permanen

Pertanyaan desain pertama bukan “Berapa kali kita harus melakukan retry?”

Pertanyaannya adalah:

Apakah percobaan berikutnya secara masuk akal dapat mengubah hasil?

Timeout saat memanggil dependency yang sehat tetapi sibuk mungkin berhasil kemudian. Retry dapat berguna.

Payload yang tidak dapat di-decode karena field wajib berisi nilai yang mustahil biasanya akan gagal dengan cara yang persis sama pada percobaan berikutnya. Mengulanginya segera hanya menambah beban tanpa menambah informasi.

Klasifikasi yang berguna dapat terlihat seperti ini:

Kegagalan Penanganan umum
network timeout retry dengan jeda
downstream overload retry dengan backoff
database tidak tersedia sementara retry
bentuk pesan tidak valid karantina
versi schema tidak didukung karantina atau arahkan ke consumer yang kompatibel
penolakan business rule yang deterministik biasanya tangani secara eksplisit, bukan sebagai infrastructure retry
exception aplikasi yang tidak diketahui retry terbatas, lalu karantina

Kategorinya bergantung pada aplikasi. Yang penting adalah membuat klasifikasi secara sengaja, bukan menganggap setiap exception dapat di-retry.

Kegagalan yang tidak diketahui perlu perhatian khusus. Kegagalan dependency sementara dapat muncul sebagai exception generik, begitu pula bug deterministik. Retry terbatas menjadi kompromi: beri kesempatan untuk pulih dari masalah sementara tanpa membuat loop retry tak berujung.

Batasi retry otomatis

Consumer tidak seharusnya melakukan retry selamanya hanya karena queue dapat mengirimkan ulang pesan.

Secara konseptual, lifecycle perlu memiliki batas:

delivery 1
   -> kegagalan yang dapat di-retry
delivery 2
   -> kegagalan yang dapat di-retry
delivery 3
   -> kegagalan yang dapat di-retry
retry budget habis
   -> dead-letter queue

Broker yang berbeda mengekspresikan batas tersebut dengan cara berbeda. Sebagian menghitung delivery atau receive. Yang lain menggunakan rejection, delivery limit, TTL, atau routing policy untuk menentukan kapan pesan menjadi dead-lettered.

Jangan menyalin jumlah retry dari sistem lain tanpa mempertimbangkan timing sistem Anda sendiri.

Misalkan jeda retry kira-kira:

5 detik
30 detik
2 menit
10 menit

Empat retry mencakup jendela pemulihan yang sangat berbeda dibanding empat redelivery langsung. Retry budget yang berguna bergantung pada jumlah percobaan dan jeda antarpercobaan.

Target praktisnya adalah memberi kegagalan sementara cukup waktu untuk pulih sambil menetapkan batas atas yang jelas pada pemrosesan yang sia-sia.

Hindari retry storm langsung

Loop retry yang terbatas tetap dapat mengganggu jika setiap kegagalan langsung di-retry.

Bayangkan dependency bersama tidak tersedia dan 10.000 consumer gagal hampir bersamaan. Jika semua pesan langsung di-requeue, dependency dapat menerima lonjakan baru tepat ketika mulai pulih.

Delay dan backoff mengurangi sinkronisasi tersebut.

Jadwal retry konseptual dapat berupa:

attempt 1 -> proses sekarang
attempt 2 -> tunggu sekitar 5 detik
attempt 3 -> tunggu sekitar 30 detik
attempt 4 -> tunggu sekitar 2 menit
lalu      -> karantina jika kegagalan berlanjut

Jitter dapat memvariasikan jeda tersebut agar banyak worker tidak melakukan retry pada saat yang persis sama.

DLQ dan retry policy menyelesaikan masalah yang berbeda:

  • backoff melindungi dependency yang sementara tidak sehat;
  • batas retry menghentikan pekerjaan tanpa akhir;
  • DLQ mempertahankan pesan gagal untuk investigasi berikutnya.

Menggunakan DLQ tanpa timing retry yang masuk akal tetap dapat menghasilkan retry storm sebelum pesan dikarantina.

Pertahankan konteks yang cukup untuk mendiagnosis kegagalan

Payload dead-letter tanpa konteks dapat sulit diinvestigasi.

Operator biasanya perlu menjawab pertanyaan seperti:

  • Queue atau subscription sumber mana yang menerima pesan?
  • Apa jenis event-nya?
  • Kapan pertama kali terlihat?
  • Berapa banyak delivery attempt yang terjadi?
  • Mengapa pemrosesan gagal?
  • Versi consumer mana yang menanganinya?
  • Apakah ada message ID atau event ID stabil untuk menelusuri log terkait?

Sebagian broker menambahkan metadata dead-letter secara otomatis. RabbitMQ, misalnya, mencatat alasan dead-lettering dan informasi sumber pada annotation atau header pesan. Sistem lain mengekspos receive count atau metadata redrive melalui atribut broker.

Konteks tingkat aplikasi tetap dapat berguna, tetapi jangan sembarangan memodifikasi business payload hanya untuk menambahkan data debugging. Consumer yang mengharapkan payload bertanda tangan atau tervalidasi schema dapat menolak pesan yang dimodifikasi karena alasan baru.

Utamakan metadata broker, atribut pesan, tracing identifier, dan log yang dapat dikorelasikan dengan event ID yang stabil.

Hindari juga menaruh secret atau data pribadi yang tidak perlu dalam atribut diagnostik. DLQ sering memiliki umur operasional lebih panjang dan akses debugging lebih luas daripada jalur pemrosesan normal.

Perlakukan DLQ sebagai workload produksi

DLQ tidak boleh menjadi tempat penyimpanan yang dilupakan.

Jika pesan dapat masuk ke sana, sistem memerlukan sinyal observability yang memberi tahu operator ketika hal itu terjadi. Monitoring yang berguna mencakup:

jumlah pesan dead-letter
usia pesan dead-letter tertua
laju kedatangan dead-letter
queue sumber atau jenis event
alasan kegagalan dominan
keberhasilan redrive dan laju kegagalan berulang

Alert terpenting sering kali bukan sekadar “DLQ berisi setidaknya satu pesan.”

Sebagian sistem sesekali melakukan dead-letter pada pesan yang sudah dikenal dan berdampak rendah. Sistem lain memerlukan investigasi setiap kali jumlahnya berubah. Threshold alert harus mencerminkan dampak bisnis dan traffic normal.

DLQ yang tumbuh cepat dapat menandakan regression deployment, ketidakcocokan schema, credential kedaluwarsa, perubahan policy downstream, atau output producer yang malformed. Laju kedatangan sering memberi informasi lebih banyak daripada jumlah absolut yang tersimpan.

Simpan pesan gagal cukup lama untuk investigasi

Retention adalah bagian dari desain pemulihan.

Jika queue sumber menyimpan pesan selama beberapa hari tetapi DLQ menghapusnya lebih cepat, operator dapat kehilangan satu-satunya salinan tidak lama setelah karantina. Sebaliknya, menyimpan kegagalan tanpa batas dapat mengakumulasi data sensitif atau usang.

Pilih periode retention DLQ berdasarkan:

  • waktu deteksi insiden;
  • perkiraan waktu investigasi;
  • prosedur pemulihan dan replay;
  • persyaratan data retention;
  • ukuran dan laju kedatangan pesan gagal.

Perilaku broker dapat berbeda secara halus. Amazon SQS, misalnya, mendokumentasikan perilaku retention dan age secara terpisah untuk standard queue dan FIFO queue, sehingga usia yang tampak pada pesan dead-letter tidak boleh diasumsikan mengikuti satu aturan universal.

Jika semantik retention penting secara operasional, verifikasi untuk broker tertentu alih-alih menyimpulkannya hanya dari istilah “dead-letter queue”.

Jangan melakukan redrive secara membabi buta

Momen berbahaya dalam workflow DLQ sering kali bukan saat karantina, melainkan saat replay.

Misalkan 50.000 pesan gagal karena deployment consumer memperkenalkan bug deterministik. Setelah bug diperbaiki, memindahkan seluruh 50.000 pesan kembali ke queue sumber sekaligus dapat membebani consumer yang baru pulih atau database downstream.

Urutan yang lebih aman adalah:

1. identifikasi kegagalan dominan
2. perbaiki atau hilangkan akar masalah
3. pilih sampel kecil yang representatif
4. redrive sampel tersebut
5. verifikasi pemrosesan dan side effect berhasil
6. tingkatkan laju redrive secara bertahap
7. pantau queue sumber dan dependency downstream

Ini mengubah redrive menjadi operasi pemulihan terkontrol, bukan lonjakan traffic berikutnya.

Sebagian broker menyediakan kontrol redrive eksplisit. Amazon SQS, misalnya, mendukung pemindahan pesan DLQ kembali ke sumber atau destination khusus dan dapat membatasi redrive velocity. Dokumentasinya secara khusus menyarankan memulai dengan laju kecil lalu meningkatkannya sambil memantau destination.

Meski broker Anda tidak memiliki kontrol laju native, recovery worker dapat menerapkan prinsip yang sama.

Buat replay aman sebelum Anda membutuhkannya

Redrive berarti pesan diproses lagi setelah setidaknya satu percobaan sebelumnya. Karena itu, side effect duplikat menjadi perhatian utama.

Pertimbangkan consumer yang melakukan tindakan berikut:

charge card
tulis state order
acknowledge message

Jika charge kartu berhasil tetapi proses gagal sebelum acknowledgement, pesan dapat dikirim lagi. Replay DLQ berikutnya dapat menciptakan kesempatan lain untuk mengulangi charge.

Karena itu, jalur replay membutuhkan disiplin idempotency yang sama dengan redelivery biasa.

Event ID stabil, idempotency key, uniqueness constraint, atau transactional deduplication record dapat membantu consumer mengenali pekerjaan yang sudah commit.

DLQ tidak boleh dianggap sebagai bukti bahwa tidak ada side effect yang terjadi. DLQ hanya membuktikan bahwa pemrosesan normal tidak mencapai definisi keberhasilan menurut broker.

Bedakan poison message dari hasil bisnis

Tidak setiap tindakan bisnis yang ditolak seharusnya masuk DLQ.

Misalkan event order valid secara sintaksis dan sepenuhnya dapat diproses, tetapi business rule menyatakan order tidak dapat dipenuhi karena telah dibatalkan.

Itu mungkin hasil domain normal, bukan kegagalan infrastruktur.

Jika keputusan bisnis yang memang diharapkan dikirim ke DLQ, queue menjadi penuh noise dan operator tidak lagi dapat membedakan pemrosesan yang rusak dari hasil yang sah.

Desain yang lebih baik sering memodelkan hasil yang diharapkan secara eksplisit:

event valid + keputusan bisnis berhasil
    -> acknowledge

event valid + penolakan bisnis yang diharapkan
    -> catat atau publish hasil domain
    -> acknowledge

tidak dapat diproses dengan andal
    -> retry jika sementara
    -> karantina jika persisten

DLQ paling berguna ketika berisi pesan yang memerlukan perhatian engineering atau operasional.

Berhati-hatilah dengan jaminan ordering

Dead-lettering dapat berinteraksi buruk dengan workload yang memerlukan ordering ketat.

Bayangkan pesan menggambarkan edit berurutan:

1. buat dokumen
2. ganti nama dokumen
3. hapus dokumen

Jika pesan 2 dikarantina sementara pesan 3 tetap berjalan, pemrosesan tidak lagi mencerminkan urutan awal.

Sebagian broker dan tipe queue menyediakan fitur ordering, tetapi memindahkan satu pesan keluar dari urutan tetap dapat mengubah semantik aplikasi. Amazon SQS secara eksplisit memperingatkan agar tidak menggunakan DLQ dengan FIFO queue ketika memutus urutan persis akan membuat workload menjadi salah.

Untuk workflow yang sensitif terhadap urutan, tentukan apa yang harus terjadi ketika satu item tidak dapat maju:

  • hentikan partition atau message group;
  • karantina seluruh urutan yang terdampak;
  • perbaiki item gagal sebelum mengizinkan item berikutnya;
  • desain ulang operasi agar setiap pesan independen.

DLQ tidak otomatis kompatibel dengan pemrosesan berurutan.

Hindari dead-letter loop

Jalur pemulihan dapat tanpa sengaja membuat siklus:

queue sumber
   -> pemrosesan gagal
DLQ
   -> redrive otomatis
queue sumber
   -> pemrosesan gagal
DLQ
   -> ...

Jika tidak ada yang berubah di antara iterasi, DLQ hanya memperpanjang loop retry tak terbatas melalui dua queue.

Karena itu, redrive harus bergantung pada bukti bahwa percobaan berikutnya dapat berhasil: bug telah diperbaiki, data referensi yang hilang telah dipulihkan, schema handler telah di-deploy, atau operator sengaja memperbaiki pesan melalui proses terdokumentasi.

Lacak kegagalan berulang setelah redrive. Pesan yang segera kembali ke DLQ adalah sinyal kuat bahwa akar masalah sebenarnya belum terselesaikan.

Jangan menganggap setiap broker melakukan dead-letter karena alasan yang sama

“Dead-letter queue” adalah istilah arsitektural yang berguna, tetapi mekanisme persisnya spesifik produk.

RabbitMQ dapat melakukan dead-letter pada pesan karena beberapa alasan, termasuk rejection tanpa requeue, TTL expiration, queue length limit, dan delivery-limit exhaustion untuk quorum queue.

Amazon SQS umumnya mengarahkan pesan ke DLQ setelah receive count melewati redrive threshold yang dikonfigurasi.

Semantik tersebut tidak dapat dipertukarkan.

Saat mengonfigurasi sistem nyata, verifikasi:

  • event apa yang menaikkan attempt counter;
  • kapan broker memutuskan melakukan dead-letter;
  • apakah atribut pesan berubah;
  • retention clock apa yang berlaku;
  • apakah ordering berubah;
  • bagaimana redrive bekerja;
  • apakah redrive mempertahankan atau membuat identifier;
  • permission apa yang diperlukan.

Pertahankan desain tingkat aplikasi agar vendor-neutral, tetapi perlakukan perilaku spesifik broker sebagai kontrak implementasi yang harus dibaca dari dokumentasi broker tersebut.

Runbook DLQ yang praktis

Dead-letter queue jauh lebih berguna ketika prosedur respons sudah ditulis sebelum insiden.

Runbook ringkas dapat terlihat seperti ini:

Deteksi
  -> alert pada kedatangan DLQ yang tidak biasa

Triage
  -> kelompokkan kegagalan berdasarkan sumber, jenis event, dan error
  -> periksa sampel yang aman

Klasifikasi
  -> masalah dependency sementara?
  -> payload producer tidak valid?
  -> regression consumer?
  -> schema tidak kompatibel?
  -> hasil bisnis yang diharapkan salah diklasifikasikan?

Perbaikan
  -> perbaiki penyebab
  -> deploy atau pulihkan dependency
  -> verifikasi idempotency

Pemulihan
  -> redrive sampel kecil
  -> verifikasi hasil
  -> tingkatkan laju replay secara bertahap

Penutupan
  -> pastikan pertumbuhan DLQ berhenti
  -> catat akar masalah
  -> tingkatkan validasi, monitoring, atau klasifikasi retry

Runbook mengubah DLQ dari queue pasif menjadi mekanisme pemulihan kegagalan yang terkontrol.

Gunakan DLQ untuk menghentikan pekerjaan sia-sia, bukan menyembunyikan kegagalan

Desain DLQ terbaik dimulai sebelum pesan mencapai dead-letter queue.

Klasifikasikan kegagalan, lakukan retry hanya ketika percobaan lain memiliki peluang masuk akal untuk berhasil, tambahkan jeda untuk kegagalan sementara, dan tetapkan retry budget yang terbatas. Ketika budget habis, karantina pesan dengan konteks yang cukup untuk investigasi.

Lalu jadikan karantina itu berguna secara operasional: pantau, simpan pesan cukup lama untuk pemulihan, pertahankan idempotency saat replay, hormati batasan ordering, dan lakukan redrive secara bertahap hanya setelah penyebab dasarnya berubah.

Dead-letter queue seharusnya membuat kegagalan persisten terlihat dan dapat dipulihkan. Jika hanya memindahkan pesan rusak agar tidak terlihat, DLQ menyembunyikan masalah alih-alih menyelesaikannya.