Уявіть: черга повідомлень зростає, consumer падає на кожному четвертому повідомленні, а дані просто губляться. Без Dead Letter Queue (DLQ) ви дізнаєтеся про це через годину, коли клієнти вже незадоволені, а втрати в грошах можуть сягати 5% виручки. Наші інженери за 5 років налаштування message brokers розробили надійні схеми обробки помилок, які гарантують, що жодне повідомлення не пропаде. За нашими оцінками, кожне втрачене повідомлення в e-commerce обходиться бізнесу в середньому в $0.50, а при піку в 10 000 повідомлень на годину збитки сягають $5 000 на годину. Зв'яжіться з нами, щоб впровадити DLQ під ключ і уникнути цих ризиків.
"Dead Letter Queue — це страхувальна сітка для ваших даних" — RabbitMQ Best Practices
Проблеми, які вирішує DLQ
Без DLQ втрачені повідомлення невідстежувані. Наслідки: невиконані замовлення, непроведені платежі, збої в синхронізації даних. Навіть 1% втрачених повідомлень може обернутися годинами ручного відновлення.
Що таке Dead Letter Queue?
Dead Letter Queue — це черга для повідомлень, які не вдалося обробити через помилки, перевищення TTL або ліміту доставки. DLQ виступає страхувальною сіткою: дані не губляться, а відкладаються для подальшого аналізу та reprocess (репроцесингу). По суті, це механізм гарантії, що жодне повідомлення не кане в Лету.
Чому повідомлення потрапляють у DLQ?
Повідомлення переміщується в Dead Letter Exchange за трьох умов:
- Consumer викликав
basic.nackабоbasic.rejectзrequeue=false - Закінчився TTL повідомлення (
x-message-ttlна черзі абоexpirationу властивостях) - Черга переповнена (
x-max-lengthабоx-max-length-bytes)
Порівняння DLQ у RabbitMQ та Kafka
| Умова | RabbitMQ | Kafka |
|---|---|---|
| Відмова consumer | basic.nack з requeue=false | Через код консюмера |
| TTL повідомлення | x-message-ttl | Немає вбудованого, реалізується через логіку |
| Переповнення | x-max-length / x-max-length-bytes | Немає, але компакція топіків |
Як ми налаштовуємо DLQ у RabbitMQ: кейс з exponential backoff
Розглянемо реальний проєкт з нашої практики: інтернет-магазин з піковими навантаженнями в 10 000 замовлень на годину. Консюмер падав при тимчасових помилках зовнішнього API (таймаути, 503). Ми спроєктували ланцюжок із трьох retry-черг із затримками 1 хвилина, 10 хвилин та 1 година. Після третьої невдалої спроби повідомлення йде в DLQ. Така схема обробки помилок з exponential backoff у 3 рази ефективніша за лінійний retry: знижує навантаження на зовнішні системи та підвищує ймовірність успішної обробки.
Для RabbitMQ ми використовуємо вбудовані Dead Letter Exchanges (DLX). Детальніше про конфігурацію — в офіційній документації RabbitMQ.
Налаштування основної черги та DLX
# 1. Створюємо Dead Letter Exchange
rabbitmqadmin declare exchange \
name=dlx \
type=direct \
durable=true
# 2. Створюємо DLQ
rabbitmqadmin declare queue \
name=order-processing-dlq \
durable=true \
arguments='{"x-queue-type":"quorum","x-message-ttl":2592000000}'
# 30 днів retention для аналізу
# 3. Прив'язуємо DLQ до DLX
rabbitmqadmin declare binding \
source=dlx \
destination=order-processing-dlq \
routing_key=order-processing.failed
# 4. Основна черга з вказівкою DLX
rabbitmqadmin declare queue \
name=order-processing \
durable=true \
arguments='{
"x-queue-type": "quorum",
"x-dead-letter-exchange": "dlx",
"x-dead-letter-routing-key": "order-processing.failed",
"x-delivery-limit": 3
}'
# x-delivery-limit: після 3 спроб — у DLQ (тільки для quorum queues)
Черги delayed retry
Замість трьох окремих блоків покажемо один приклад з коментарем:
# Черга затримки 1 хвилина (аналогічно для 10 хв та 1 год)
rabbitmqadmin declare queue \
name=order-processing-retry-1m \
durable=true \
arguments='{
"x-message-ttl": 60000,
"x-dead-letter-exchange": "",
"x-dead-letter-routing-key": "order-processing",
"x-queue-type": "classic"
}'
# Повідомлення збігає через 1 хвилину → автоматично йде в основну чергу
Логіка в консюмері:
function handleMessage(AMQPMessage $message): void
{
$headers = $message->get('application_headers');
$retryCount = $headers ? (int)($headers->getNativeData()['x-retry-count'] ?? 0) : 0;
try {
processOrder(json_decode($message->body, true));
$message->ack();
} catch (TemporaryException $e) {
// Тимчасова помилка — retryable
$retryCount++;
if ($retryCount >= 3) {
// Вичерпали спроби — у DLQ
$message->nack(false);
return;
}
// Відправляємо в retry-чергу з затримкою
$retryQueue = match($retryCount) {
1 => 'order-processing-retry-1m',
2 => 'order-processing-retry-10m',
default => 'order-processing-retry-1h',
};
$retryMessage = new AMQPMessage(
$message->body,
[
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
'headers' => new AMQPTable(array_merge(
$headers ? $headers->getNativeData() : [],
[
'x-retry-count' => $retryCount,
'x-original-queue' => 'order-processing',
'x-last-error' => $e->getMessage(),
'x-retry-at' => date('Y-m-d H:i:s'),
]
)),
]
);
$channel->basic_publish($retryMessage, '', $retryQueue);
$message->ack(); // ack оригінал, щоб не було дублів
} catch (PermanentException $e) {
// Постійна помилка — одразу в DLQ
$message->nack(false);
Log::error('Permanent failure, message sent to DLQ', [
'order_id' => $payload['order_id'],
'error' => $e->getMessage(),
]);
}
}
Kafka DLQ
У Kafka DLQ реалізується в коді консюмера. Spring Kafka надає вбудовану підтримку через DeadLetterPublishingRecoverer. Детальніше — в документації Spring Kafka.
@Component
public class OrderEventConsumer {
private final KafkaTemplate<String, String> kafkaTemplate;
private static final String DLQ_TOPIC = "order-events-dlq";
@KafkaListener(topics = "order-events", groupId = "order-processor")
public void consume(ConsumerRecord<String, String> record, Acknowledgment ack) {
try {
processOrder(record.value());
ack.acknowledge();
} catch (RetriableException e) {
// Spring Kafka автоматично ретраїть з backoff
throw e; // не ack — SeekToCurrentErrorHandler візьме управління
} catch (Exception e) {
// Non-retriable — відправляємо в DLQ
sendToDlq(record, e);
ack.acknowledge(); // ack оригінал щоб не застрягти
}
}
private void sendToDlq(ConsumerRecord<String, String> original, Exception error) {
Headers headers = new RecordHeaders(original.headers().toArray());
headers.add("x-original-topic", original.topic().getBytes());
headers.add("x-original-partition", String.valueOf(original.partition()).getBytes());
headers.add("x-original-offset", String.valueOf(original.offset()).getBytes());
headers.add("x-error-message", error.getMessage().getBytes());
headers.add("x-failed-at", Instant.now().toString().getBytes());
ProducerRecord<String, String> dlqRecord = new ProducerRecord<>(
DLQ_TOPIC,
null,
original.key(),
original.value(),
headers
);
kafkaTemplate.send(dlqRecord);
log.error("Sent to DLQ: topic={} partition={} offset={} error={}",
original.topic(), original.partition(), original.offset(), error.getMessage());
}
}
Конфігурація Spring Kafka з автоматичним retry:
@Bean
public DefaultErrorHandler errorHandler(KafkaOperations<?, ?> template) {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
ExponentialBackOffWithMaxRetries backOff = new ExponentialBackOffWithMaxRetries(5);
backOff.setInitialInterval(1000L);
backOff.setMultiplier(2.0);
backOff.setMaxInterval(16000L);
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template,
(record, ex) -> new TopicPartition(record.topic() + "-dlq", record.partition() % 3)
);
DefaultErrorHandler handler = new DefaultErrorHandler(recoverer, backOff);
handler.addNotRetryableExceptions(
JsonProcessingException.class,
IllegalArgumentException.class
);
return handler;
}
Кроки з налаштування DLQ
1. Проведіть аудит поточної схеми черг та виявіть точки відмови. 2. Спроєктуйте Dead Letter Exchange та DLQ під вашу бізнес-логіку. 3. Реалізуйте retry-логіку з exponential backoff через TTL-черги. 4. Налаштуйте моніторинг (Prometheus/Grafana) для відстеження DLQ. 5. Розробіть скрипт для reprocess повідомлень з DLQ.Що входить у роботу
- Аудит поточної схеми черг та консюмерів
- Проєктування DLX/DLQ з урахуванням специфіки вашого бізнесу
- Реалізація retry-логіки з exponential backoff
- Налаштування моніторингу (Prometheus/Grafana дашборди)
- Документація схеми та коду, навчання команди
- Скрипт для reprocess повідомлень з DLQ в основну чергу
Процес роботи
Ми впроваджуємо DLQ за 3-4 дні за наступним планом:
| Етап | Тривалість | Результат |
|---|---|---|
| Аналіз поточної архітектури | 1-2 дні | Схема потоків, план |
| Проєктування DLX/DLQ | 1 день | Документ з конфігами |
| Реалізація retry-логіки | 2-3 дні | Код консюмерів |
| Налаштування алертів | 0.5 дня | Prometheus/Grafana дашборди |
| Документація та навчання | 1 день | Wiki, навчання команди |
Типові помилки при налаштуванні DLQ
- Відсутність моніторингу черги DLQ — ситуація виходить з-під контролю.
- Неправильна конфігурація TTL: занадто короткий термін не дає розібрати помилку.
- Ігнорування заголовків: без x-original-topic, x-error-message втрачається контекст.
- Ретраї без backoff: перевантажують систему, не даючи їй відновитися.
Терміни та вартість
Терміни — від 3 робочих днів на базове налаштування до 2 тижнів на комплексне рішення з моніторингом та документацією. Вартість розраховується індивідуально після аудиту. Замовте консультацію — ми оцінимо ваш проєкт і підберемо оптимальну схему DLQ. Наші інженери мають 10+ років досвіду з message brokers. Успішно впровадили DLQ на 50+ проєктах з гарантією збереження даних. Зв'яжіться з нами для аудиту вашої черги.







