Dead Letter Queue pattern: poison message handling, replay flow, alerting on DLQ. Multi-scenario animated explainer showing what happens without DLQ (consumer blocked), with DLQ (failed messages quarantined, processing continues), replay after fix, and silent-swallow antipattern.
В любой асинхронной системе рано или поздно появляется сообщение, которое consumer не может обработать. Payload не проходит schema validation, внешний API стабильно отвечает 400, новая версия producer-а изменила контракт, consumer падает на редком поле, срок бизнес-действия уже истёк. Если такое сообщение бесконечно retry-ить, оно блокирует очередь или partition. Если его молча удалить, система теряет данные без следа. Dead Letter Queue даёт третий вариант: после ограниченного числа попыток вынести сообщение в карантин, сохранить контекст ошибки, поднять alert и дать оператору controlled replay после исправления причины.
DLQ важен не только как техническая очередь. Это часть production-контракта для message-driven архитектуры. Она отвечает на вопросы: какие сообщения не обработались, почему они не обработались, кто должен посмотреть, можно ли их повторить, что будет с идемпотентностью при replay. Без этих ответов команда обычно обнаруживает проблему поздно: Kafka consumer стоит на одном offset, RabbitMQ крутит nack/requeue, отчёты не сходятся, а в логах уже нет нужного stack trace.
Паттерн связан с message-queues, retry-backoff, transactional-outbox и observability. Retry решает временные сбои, DLQ решает permanent или unknown failures. Outbox помогает не потерять исходные события. Observability делает DLQ не кладбищем сообщений, а управляемым операционным процессом.
DLQ — это карантин, а не мусорная корзина. Сообщение попадает туда не потому, что «оно больше не нужно», а потому что автоматическая обработка больше не имеет права повторяться без человеческого или отдельного программного решения. Карантин должен быть видимым: размер DLQ, скорость поступления, возраст самого старого сообщения, типы ошибок и возможность безопасного replay.
Другая важная мысль: DLQ не исправляет причину. Если consumer не умеет читать новую схему, replay без deploy-а вернёт все сообщения в DLQ. Если downstream отклоняет старые заказы по бизнес-правилу, повторная отправка создаст шум. Если handler неидемпотентный, replay может сделать хуже, чем исходная ошибка.
Диаграмма показывает producer, основной main topic, consumer, отдельную DLQ (quarantine), мониторинг и replay tool. Поток выглядит просто: producer публикует сообщения в основную очередь, consumer обрабатывает их, после N неудачных попыток broker или consumer framework переносит сообщение в DLQ. Отдельная связь из DLQ в monitor показывает, что попадание сообщения в карантин должно становиться метрикой и alert-ом, а не тихим боковым эффектом.
Replay tool связан и с DLQ, и с main queue. Это осознанно: replay не должен быть скрытой кнопкой «перекинуть всё обратно». Инструмент читает envelope, показывает оригинальный topic, headers, offset, attempts, последнюю ошибку, версию consumer-а и timestamps. Только после фикса root cause он re-inject-ит сообщения в основной поток, часто с replay_marker header, batch limits и rate limit.
Диаграмма также показывает негативный сценарий: без DLQ poison message блокирует обработку следующих сообщений. Особенно важно для Kafka, где consumer group может застрять на одном offset, если обработчик не умеет skip-and-DLQ. Для RabbitMQ и SQS проблема выглядит иначе, но итог тот же: очередь забита повторными доставками, нормальный traffic ждёт.
Без DLQ: poison message блокирует всю partition учит, что retry не является универсальным лечением. Если payload невалиден, повтор не изменит payload. Если schema registry отклоняет событие, новая попытка через секунду даст ту же ошибку. ADR-решение: ограничить retries и иметь путь quarantine, иначе один bad message становится системным отказом.
С DLQ: после N retries — карантин, processing continues показывает правильный баланс. Сообщение не теряется, но main pipeline освобождается. Важная деталь: в DLQ должен попадать enriched envelope, а не только original payload. Без metadata расследование превращается в поиск по логам: какая версия consumer-а упала, сколько было попыток, какой offset, когда случился первый fail, какая последняя ошибка.
Debug в DLQ -> fix root cause -> replay messages показывает, что replay — часть lifecycle. Сначала оператор видит кластер сообщений с одной ошибкой, затем команда исправляет consumer, проверяет обработку новых сообщений и только потом re-inject-ит старые. ADR-решение: replay разрешён только после устранения причины и оценки идемпотентности. Альтернатива «сразу перелить назад» обычно создаёт второй инцидент.
Антипаттерн: catch-all -> DLQ без alerting -> silent loss показывает самую опасную форму DLQ. Если catch (Exception) отправляет всё в DLQ и ack-ает сообщение, система выглядит здоровой: lag не растёт, consumer жив, pipeline идёт дальше. Но бизнес-факты исчезают из основного процесса. Правильное решение: whitelist expected permanent errors для DLQ, а unknown exceptions должны fail loud, поднимать alert или останавливать partition до разбора.
ADR: DLQ или бесконечный retry? Контекст: transient failures встречаются часто, но permanent failures тоже неизбежны. Бесконечный retry сохраняет надежду на восстановление, но блокирует throughput и маскирует poison messages. DLQ после N attempts сохраняет данные и освобождает pipeline, но требует операционного процесса. Решение: retry с backoff для временных ошибок, затем DLQ для исчерпанных попыток или явно permanent failures.
ADR: кто переносит в DLQ — broker или consumer? SQS, RabbitMQ и Pub/Sub дают нативные механизмы dead-letter policy. Kafka чаще требует реализации в consumer framework: Spring Kafka, Kafka Connect или собственный handler. Broker-level проще и единообразнее, но иногда не знает бизнес-классификацию ошибки. Consumer-level гибче, но его легче испортить catch-all логикой. Выбор зависит от платформы и того, где доступна информация о типе failure.
ADR: DLQ как topic или как таблица расследования? Topic сохраняет потоковую природу и удобен для replay. Таблица удобна для UI, фильтрации, ручной triage и enriched metadata. В серьёзных системах часто есть оба слоя: broker DLQ как источник сообщений и индексированное хранилище для расследования.
ADR: alert на любое сообщение или порог? Строгая позиция dlq.size > 0 = alert хороша для критических финансовых потоков. Для высокообъёмной аналитики допустимы пороги по rate, age и error class. Но «никто не смотрит DLQ» не является вариантом. Минимум: alert на spike, возраст старейшего сообщения и weekly review.
AWS SQS поддерживает redrive policy: после maxReceiveCount сообщение уходит в указанную DLQ. RabbitMQ использует Dead Letter Exchange и routing key для сообщений, которые reject-нуты, истекли по TTL или превысили limit очереди. Google Pub/Sub имеет dead letter policy на subscription. Pulsar поддерживает dead letter topic. Kafka не имеет единого встроенного DLQ для обычных consumer-ов, поэтому паттерн реализуется в Kafka Connect, Spring Kafka, Flink jobs или собственном consumer layer.
В платежах DLQ нужен для webhook-ов и событий биллинга: нельзя потерять факт оплаты, но нельзя бесконечно блокировать очередь из-за одного старого payload. В e-commerce DLQ помогает расследовать заказы, которые не ушли в fulfillment. В data pipelines DLQ отделяет плохие события от общего потока, сохраняя возможность последующей очистки и backfill.
Главный anti-pattern — silent swallow: поймать любое исключение, отправить сообщение в DLQ, ack-нуть и не алертить. Это превращает DLQ в механизм тихой потери данных.
Вторая ошибка — хранить только payload. Нужны headers, source topic, partition, offset, attempt count, first/last failure time, last error, stack trace, producer и consumer versions, correlation id, trace id. Без этого невозможно понять, является ли проблема контрактной, инфраструктурной или бизнесовой.
Третья ошибка — replay без идемпотентности. Если обработчик списывает деньги, отправляет письмо или создаёт shipment, повтор может удвоить эффект. Перед replay должны быть idempotency keys, de-duplication или явное решение, что дубль безопасен.
Четвёртая ошибка — одинаково обрабатывать transient и permanent errors. 503 от downstream и ValidationError: missing required field требуют разных политик. Первый случай должен идти через retry/backoff и circuit breaker, второй часто сразу или после малого числа попыток уходит в DLQ.
Пятая ошибка — бесконечно хранить DLQ без владельца. Сообщения стареют: бизнес-смысл истекает, схемы меняются, replay становится опаснее. Для DLQ нужны retention, owner, runbook и решение по discard/archive.
DLQ не нужен для одноразовых best-effort событий, где потеря заранее допустима и зафиксирована в контракте. Например, часть телеметрии может быть sampled или dropped без ручного расследования. Но это должно быть явно записано, а не случайно получиться из отсутствия DLQ.
DLQ не заменяет backpressure, circuit breaker и retry policy. Если downstream лежит, складывать миллионы сообщений в DLQ вместо управления потоком неправильно. DLQ также не должен быть способом обойти schema governance: если producer и consumer часто расходятся, нужно версионирование событий и compatibility checks.
Не используйте DLQ как архив всех ошибок приложения. Для этого есть logs, traces и error tracking. В DLQ должны попадать конкретные сообщения, судьбу которых надо решить: replay, transform, discard with approval или manual repair.
message-queues — базовая семантика доставки, ack/nack, ordering и consumer groups.retry-backoff — как отличать временные сбои от permanent failures.transactional-outbox — как надёжно публиковать события из write transaction.observability-pillars — какие метрики, логи и traces нужны вокруг DLQ.