🎯 什麼情境該想到我

當你「有訊息無法被成功投遞或處理(重試多次仍失敗、過期、找不到目的地)」的時候:需要一個地方收容它,而不是無限卡在原通道。

⚙️ 怎麼用(步驟 / 公式)

意圖:當訊息系統無法投遞一則訊息時,把它移到專門的死信通道,而不是丟棄或阻塞。

做法 / 結構要點:

  • 由訊息系統(而非應用邏輯)在投遞失敗、逾時、過期或重試上限時,把訊息轉入死信通道。
  • 死信通道集中收容這些訊息,供監控、告警、人工或自動重處理。
  • 保護正常通道不被投不出去的訊息塞住。

★★ 各家 broker 的實際機制(差異很大,選型時最該問的一題)

查證 2026-09-25。

broker機制★ 關鍵細節
RabbitMQ 4.xDead Letter Exchange(DLX),用 policy 設定★ 四個觸發條件見下。官方建議用 policy 而非硬寫 x-arguments
SQSRedrive policy:maxReceiveCount 超過就進 DLQ★ 計數是「被接收次數」,處理超時被重送也算一次
Kafka★★ 沒有原生 DLQ要自己寫(catch → produce 到 xxx.dlq topic);Kafka Connect 與 Streams 有各自的 DLQ 設定
NATS JetStreammax_deliver + advisory 事件超過次數會發 advisory,要自己接

RabbitMQ 的四個 dead-letter 觸發條件(官方文件原文,2026-09-25):

① 訊息被 nack/reject 且 requeue=false(AMQP 1.0 的 rejected outcome)
② 訊息因 per-message TTL 過期
③ 訊息因佇列超過長度上限被丟棄
④ ★ 訊息被退回 quorum queue 的次數超過 delivery-limit
★★ 但:整個佇列過期時,裡面的訊息「不會」被 dead-letter —— 直接消失

★★ 最後那一條是最容易踩的陷阱:你設了 DLX,也設了 queue TTL,
結果佇列整個過期時訊息靜默消失
,DLQ 裡什麼都沒有。

★ Kafka 沒有原生 DLQ,這是選型時最該知道的一件事。
你必須自己實作「重試 N 次 → 發到 dlq topic」,而且要自己管重試計數(放在標頭)。

★ Go 的 DLQ 實作骨架(Kafka 版)

const maxAttempts = 5
 
func consume(ctx context.Context, msg Message) error {
    attempt := attemptFromHeader(msg) + 1        // ★ 重試次數放標頭,Kafka 不幫你記
 
    if err := handle(ctx, msg); err != nil {
        if errors.Is(err, ErrInvalidMessage) {    // ★ 永久錯誤 → 不重試
            return toDLQ(ctx, msg, err, attempt, "invalid")
        }
        if attempt >= maxAttempts {
            return toDLQ(ctx, msg, err, attempt, "exhausted")
        }
        // ★ 重試:延遲重發(Kafka 沒有原生延遲投遞 → 用分層 retry topic)
        return republish(ctx, retryTopicFor(attempt), msg, attempt)
    }
    return nil
}

★ 「分層 retry topic」是 Kafka 的標準做法:
retry.5s / retry.1m / retry.10m 各一個 topic,消費者先 sleep 到時間再處理。
⚠️ 直接在原 topic 上重試會阻塞整個分區(head-of-line blocking)——
這是 Kafka 當任務佇列時最痛的一點(見 訊息傳遞)。

★★ DLQ 訊息一定要帶的欄位

type DeadLetter struct {
    Original       json.RawMessage   `json:"original"`         // ★ 原始 body,不改一個位元
    Headers        map[string]string `json:"headers"`
    SourceTopic    string            `json:"source_topic"`     // ★ 重放要送回哪裡
    Partition      int32             `json:"partition"`
    Offset         int64             `json:"offset"`           // ★ 能回去原地查
    Attempts       int               `json:"attempts"`
    FailureKind    string            `json:"failure_kind"`     // ★★ invalid / transient / exhausted
    LastError      string            `json:"last_error"`
    FirstFailedAt  time.Time         `json:"first_failed_at"`
    ServiceVersion string            `json:"service_version"`  // ★ 哪一版判定它失敗
}

★ FailureKind 是最有價值的一欄:它讓「上游發壞資料」和「我的服務掛了」
在指標上分得開(見 無效訊息通道)。

★★ DLQ 必須配的三件事,以及各自的失敗樣貌

缺了什麼會發生什麼
沒有 DLQ★ 毒訊息卡住整個佇列——一直重試同一筆,後面全部堵住
有 DLQ、沒有告警★ DLQ 累積幾萬筆沒人看 = 資料默默遺失,而你以為有防護
有告警、沒有重放★ 知道壞了但救不回來,只能人工補資料
有重放、消費端不冪等★★ 重放製造重複副作用(重複扣款、重複寄信)

★ 告警要看「速率」而非「總數」:
DLQ 有 3 筆是資料品質問題(開票追蹤);一分鐘進 500 筆是事故(立刻叫人)。

★ 兩個常被忽略的細節

① ★ DLQ 自己也會滿、也有 retention
   SQS 的 DLQ 仍受 retention 限制(預設 4 天,最長 14 天,查證 2026-09-25)
   → ★ 「訊息進了 DLQ 就安全了」是錯的。兩週內沒人處理照樣消失
   → 需要長期保留就把 DLQ 訊息落地到資料庫/S3([[訊息存放區]])

② ★ 重放的順序
   DLQ 裡的訊息重放回主通道時,會插在「當前訊息」後面 ⇒ 順序亂了
   → ★ 對順序敏感的流程,重放前要先暫停該實體的處理,或用版本號防舊蓋新

★ 兄弟辨析

差別
無效訊息通道★ 應用程式判定「看不懂」;DLQ 是 broker 判定「交付不了」
訊息過期TTL 過期是進 DLQ 的原因之一(RabbitMQ 條件②)
繞道刻意把訊息導去別處(除錯/驗證),不是失敗處理
訊息存放區保存所有訊息以供查詢;DLQ 只存失敗的

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 與無效訊息通道分工:死信是「系統無法投遞/處理」,無效訊息是「接收端判定內容語意有問題」。

  • 死信通道會堆積,需要監看與處理策略,否則問題只是被藏起來。

  • ★★ Kafka 沒有原生 DLQ。重試計數、延遲重試、DLQ topic 全部要自己寫。

  • ★★ RabbitMQ:整個佇列過期時,訊息不會進 DLX,直接消失。 設了 DLX 也救不到。

  • ★ 在 Kafka 原 topic 上重試會阻塞整個分區。要用分層 retry topic。

  • ★ SQS 的 maxReceiveCount 算的是「被接收次數」,處理超時被重送也算——所以處理慢會意外進 DLQ。

  • ★★ DLQ 也有 retention(SQS 預設 4 天、最長 14 天)。「進了 DLQ 就安全」是錯的。

  • ★ 有 DLQ 沒告警 = 資料默默遺失,而你以為有防護。告警要看速率而非總數。

  • ★★ 重放前必須確認消費端冪等,否則重放製造重複扣款/重複寄信。

  • ★ 重放會打亂順序。對順序敏感的流程要先暫停該實體的處理,或用版本號防舊蓋新。

  • DLQ 訊息要帶 source topic/partition/offset/failure kind/service version,否則救不回來也查不出原因。

🔗 相關工具