Một message/job luôn fail: payload corrupt, schema đổi, bug logic, id không tồn tại mà code throw. Queue at-least-once + retry vô hạn → worker đốt CPU quanh message đó; hoặc (FIFO chặt) chặn cả lane phía sau.
Đó là poison message. Thuốc giải vận hành: giới hạn attempt, tách ra dead-letter queue (DLQ) / quarantine, alert, rồi quyết định fix & replay — không phải “retry thêm cho vui”.
Demo: Queue + poison → DLQ sau N attempts. Cùng họ với BullMQ failed / delayed.
Vòng đời lành mạnh
waiting → active → completed
↘ failed → (backoff) delayed → waiting … × max
↘ DLQ / dead (hết attempt hoặc lỗi “không retry”)
- Transient (timeout, 503): retry + backoff/jitter.
- Permanent (validation, 404 business, poison): đừng đốt hết max attempts nếu nhận ra sớm — fail to DLQ ngay.
Phân loại lỗi
function routeError(err) {
if (err.code === "VALIDATION" || err.code === "NOT_FOUND") {
return "dead"; // không retry
}
if (err.code === "TIMEOUT" || err.status === 503) {
return "retry";
}
return "retry"; // default cẩn thận; unknown hay retry có budget
}
“Retry mọi Exception” biến bug deploy thành storm trên cùng một bad payload.
DLQ làm gì (và không làm gì)
-
Làm: giữ payload + metadata (attempts, last
error, stack rút gọn, timestamp) để người xem; metric
dlq_depth; alert khi tăng. - Không: coi DLQ là “xong việc”. Message trong DLQ = business có thể đang kẹt (đơn chưa xử lý).
- Replay: sau khi fix code/data, đẩy lại main queue có chủ đích — kèm idempotency vì có thể đã side-effect dở.
Sketch consumer
async function handle(job) {
try {
await process(job.payload);
await job.complete();
} catch (e) {
if (routeError(e) === "dead" || job.attempts >= job.maxAttempts) {
await job.moveToDlq({
error: String(e),
attempts: job.attempts,
payload: job.payload,
});
alertIfNeeded();
return;
}
await job.retry({ delay: backoff(job.attempts) });
}
}
FIFO & head-of-line
Queue yêu cầu thứ tự chặt: poison ở đầu có thể block consumer group. Chiến lược: timeout attempt ngắn + nhanh vào DLQ; hoặc lane “skip poison” có audit — trade-off ordering vs liveness. Đây là lý do nhiều hệ chọn per-key ordering thay vì global FIFO.
Quan sát
job_failed_totaltheoerror_codedlq_enqueued/ depth / age of oldest- Tỷ lệ fail lặp cùng
payload_hash→ poison thật
Một câu để nhớ
Retry là cho lỗi tạm. DLQ là cho lỗi “không tự hết”. Không có max attempts + nơi chứa message độc, queue của bạn chỉ là vòng lặp đốt tiền quanh một payload hỏng.