Dead Letter-wachtrij en veerkracht in asynchrone systemen
In elk asynchrone systeem mislukken berichten. Een verkeerd opgemaakte payload, een onbereikbare downstream-service, een bug in de consumentencode – zonder veiligheidsmechanisme gaan deze berichten verloren of blokkeer de verwerking van volgende. Daar Wachtrij met dode letters het is dat mechanisme: vangt onverwerkbare berichten op, isoleert ze in een aparte wachtrij en maakt analyse en herverwerking mogelijk selectief zonder de hoofdstroom te onderbreken.
Het probleem: wat er gebeurt met berichten die mislukken
Asynchrone berichtensystemen zoals SQS, SNS, Kafka en EventBridge gebruiken het patroon minimaal één keer bezorgen: een bericht wordt minstens één keer afgeleverd, maar het kan meerdere keren worden afgeleverd (duplicaten in geval van nieuwe poging). Dit creëert twee kritieke scenario's:
- Tijdelijke fouten: De downstream-service is tijdelijk niet beschikbaar. Automatisch opnieuw proberen lost het probleem op. Na enkele pogingen wordt het bericht correct verwerkt.
- Blijvende fouten (gifpil): het bericht is verkeerd opgemaakt en bevat gegevens die zakelijke invarianten schenden, of de code van de consument bevat een bug. Opnieuw proberen helpt niet: het bericht zal voor onbepaalde tijd blijven mislukken, waardoor mogelijk middelen worden verbruikt het blokkeren van de verwerking van volgende berichten.
De DLQ lost het tweede scenario op: na een configureerbaar aantal mislukte pogingen (maxReceiveCount in SQS,
MAX_RETRY_ATTEMPTS in Kafka), wordt het bericht verplaatst naar de wachtrij voor dode brieven
waar het op gecontroleerde wijze kan worden geanalyseerd en herverwerkt.
DLQ: het Veerkrachtcontract
- Geen gegevensverlies: Er worden geen berichten stilzwijgend verwijderd
- Probleem isolatie: Gifpillen blokkeren geen goede berichten
- Zichtbaarheid: Berichten in DLQ kunnen worden geïnspecteerd op foutopsporing
- Gecontroleerde herverwerking: nadat het probleem is verholpen, worden de berichten opnieuw verwerkt
DLQ in Amazon SQS
In SQS is de DLQ eenvoudigweg een andere SQS-wachtrij die is geconfigureerd als bestemming voor berichten
die overschrijden maxReceiveCount. Het mechanisme is gebaseerd op time-out voor zichtbaarheid:
wanneer een consument een bericht ontvangt, wordt dit voor de duur "onzichtbaar" voor andere consumenten
van de zichtbaarheidstime-out. Als het niet binnen die tijd wordt verwijderd (de consument is gefaald of gecrasht),
SQS maakt het weer zichtbaar voor een nieuwe poging.
De teller ApproximateReceiveCount wordt bij elke ontvangst verhoogd.
Wanneer het bereikt maxReceiveCount, verplaatst SQS het bericht naar de geconfigureerde DLQ.
# Configurazione DLQ per SQS con Terraform
# 1. Crea la DLQ (stessa tipologia della coda principale)
resource "aws_sqs_queue" "ordini_dlq" {
name = "ordini-queue-dlq"
message_retention_seconds = 1209600 # 14 giorni (massimo SQS)
visibility_timeout_seconds = 300 # 5 minuti per elaborare dalla DLQ
# CloudWatch alarm sulla DLQ
tags = {
Environment = "production"
Alert = "critical"
}
}
# 2. Crea la coda principale con redrive policy che punta alla DLQ
resource "aws_sqs_queue" "ordini" {
name = "ordini-queue"
visibility_timeout_seconds = 60 # 60s per elaborare ogni messaggio
receive_wait_time_seconds = 20 # long polling
message_retention_seconds = 345600 # 4 giorni
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.ordini_dlq.arn
maxReceiveCount = 3 # 3 tentativi falliti -> DLQ
})
}
# 3. CloudWatch Alarm: alert quando la DLQ ha messaggi
resource "aws_cloudwatch_metric_alarm" "dlq_not_empty" {
alarm_name = "ordini-dlq-not-empty"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "1"
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = "60"
statistic = "Sum"
threshold = "0"
alarm_description = "CRITICO: messaggi in DLQ ordini"
dimensions = {
QueueName = aws_sqs_queue.ordini_dlq.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
Inspecteer en herverwerk door DLQ SQS
// DlqReprocessor.java - Riprocessa messaggi dalla DLQ SQS
import software.amazon.awssdk.services.sqs.*;
import software.amazon.awssdk.services.sqs.model.*;
public class SqsDlqReprocessor {
private final SqsClient sqsClient;
private final String dlqUrl;
private final String mainQueueUrl;
// Riprocessa tutti i messaggi dalla DLQ verso la coda principale
public void reprocessAll() {
int reprocessed = 0;
List<Message> messages;
do {
messages = receiveMessages(dlqUrl, 10);
for (Message message : messages) {
try {
// Analizza il messaggio per capire il tipo di errore
System.out.printf("Riprocesso messaggio: id=%s, receiveCount=%s%n",
message.messageId(),
message.attributes().get(MessageSystemAttributeName.APPROXIMATE_RECEIVE_COUNT)
);
// Rimanda alla coda principale (con delay opzionale)
sqsClient.sendMessage(
SendMessageRequest.builder()
.queueUrl(mainQueueUrl)
.messageBody(message.body())
.messageAttributes(message.messageAttributes())
.delaySeconds(0)
.build()
);
// Elimina dalla DLQ
sqsClient.deleteMessage(
DeleteMessageRequest.builder()
.queueUrl(dlqUrl)
.receiptHandle(message.receiptHandle())
.build()
);
reprocessed++;
} catch (Exception e) {
System.err.println("Errore reprocessing: " + e.getMessage());
// Non eliminare: rimane in DLQ
}
}
} while (!messages.isEmpty());
System.out.printf("Reprocessing completato: %d messaggi rimandati%n", reprocessed);
}
private List<Message> receiveMessages(String queueUrl, int maxMessages) {
return sqsClient.receiveMessage(
ReceiveMessageRequest.builder()
.queueUrl(queueUrl)
.maxNumberOfMessages(maxMessages)
.waitTimeSeconds(5)
.attributeNames(QueueAttributeName.ALL)
.build()
).messages();
}
}
DLQ in AWS Lambda: functieniveau versus wachtrijniveau
In AWS Lambda kan DLQ op twee verschillende niveaus worden geconfigureerd met verschillende semantiek:
-
SQS-wachtrij DLQ: Geconfigureerd op de bron-SQS-wachtrij. Berichten worden verplaatst
in DLQ wanneer SQS groter is dan de
maxReceiveCount. Dit gebeurt Voor dat Lambda wordt aangeroepen. Dit is de aanbevolen configuratie voor Lambda + SQS. - Lambda-functie DLQ: geconfigureerd op de Lambda zelf (alleen voor asynchrone aanroepen, niet voor het in kaart brengen van gebeurtenisbronnen met SQS). Vang mislukkingen van de Lambda-aanroep op, niet de wachtrij.
# Terraform: Lambda con SQS event source e DLQ configurata sulla coda
resource "aws_lambda_function" "ordini_consumer" {
function_name = "ordini-consumer"
handler = "handler.lambda_handler"
runtime = "python3.12"
role = aws_iam_role.lambda_role.arn
timeout = 30 # 30 secondi per messaggio
# DLQ a livello di Lambda (solo per invocazioni async dirette)
dead_letter_config {
target_arn = aws_sqs_queue.lambda_dlq.arn
}
}
# SQS come event source per Lambda
resource "aws_lambda_event_source_mapping" "sqs_trigger" {
event_source_arn = aws_sqs_queue.ordini.arn
function_name = aws_lambda_function.ordini_consumer.arn
batch_size = 10
enabled = true
# Bisection: in caso di errore batch, prova prima con metà messaggi
# Aiuta a isolare il poison pill senza mandare tutti in DLQ
bisect_batch_on_function_error = true
# Report batch item failures: Lambda può indicare quali specifici
# messaggi nel batch hanno fallito (solo quelli vanno in DLQ)
function_response_types = ["ReportBatchItemFailures"]
}
ReportBatchItemFailures: gedetailleerde DLQ voor batch
// Handler Lambda Python con batch item failures
// Solo i messaggi falliti vanno in DLQ, non l'intero batch
def lambda_handler(event, context):
"""
ReportBatchItemFailures: ritorna solo i message ID falliti.
SQS mandrà in DLQ solo quelli, non il batch intero.
"""
failed_items = []
for record in event['Records']:
message_id = record['messageId']
try:
# Elabora il messaggio
payload = json.loads(record['body'])
process_ordine(payload)
print(f"Successo: {message_id}")
except PermanentError as e:
# Errore permanente: vai in DLQ subito
print(f"PERMANENTE: {message_id} - {e}")
failed_items.append({'itemIdentifier': message_id})
except TransientError as e:
# Errore transitorio: riprova (non aggiungere a failed)
# SQS ritenterà l'intero batch se almeno uno fallisce
# Con ReportBatchItemFailures, solo i falliti vengono ritentati
print(f"TRANSITORIO: {message_id} - {e}")
failed_items.append({'itemIdentifier': message_id})
return {'batchItemFailures': failed_items}
DLQ in EventBridge
EventBridge heeft zijn eigen DLQ-niveau doel: indien de levering van een evenement
naar het doel (Lambda, SQS) mislukt nadat alle geconfigureerde nieuwe pogingen zijn gedaan, wordt de gebeurtenis verzonden
in de SQS DLQ gespecificeerd in dead_letter_config van de regel.
# EventBridge DLQ per target Lambda
resource "aws_cloudwatch_event_target" "ordini_lambda" {
rule = aws_cloudwatch_event_rule.ordini.name
event_bus_name = aws_cloudwatch_event_bus.mioapp.name
arn = aws_lambda_function.ordini_consumer.arn
# Retry policy di EventBridge: quanti tentativi prima di DLQ
retry_policy {
maximum_event_age_in_seconds = 86400 # Riprova per max 24h
maximum_retry_attempts = 185 # ~exponential backoff su 24h
}
# DLQ per eventi non consegnati
dead_letter_config {
arn = aws_sqs_queue.eventbridge_dlq.arn
}
}
# L'evento in DLQ EventBridge include metadata di debug
# {
# "version": "1.0",
# "timestamp": "...",
# "requestId": "...",
# "condition": "...",
# "approximateInvokeCount": 185,
# "requestParameters": {
# "FunctionName": "ordini-consumer"
# },
# "responseParameters": {
# "statusCode": 500,
# "errorCode": "Lambda.ServiceException"
# },
# "originalEvent": { ... l'evento originale ... }
# }
Geavanceerd patroon: Probeer het opnieuw met progressieve uitstel met behulp van SQS-vertraging
Met SQS kunt u een vertraging voor één bericht (tot 15 minuten). Gecombineerd met de FIFO-wachtrij en de berichten groep, het is mogelijk om te implementeren een exponentieel patroon voor opnieuw proberen zonder andere berichten te blokkeren:
// RetryWithSqsDelay.java - Retry progressivo con SQS message delay
public class SqsExponentialRetry {
private static final int MAX_ATTEMPTS = 5;
private static final int MAX_DELAY_SECONDS = 900; // 15 minuti (max SQS)
public void handleWithRetry(String queueUrl, Message sqsMessage) {
// Leggi il numero di tentativi corrente dal message attribute
int currentAttempt = Integer.parseInt(
sqsMessage.messageAttributes()
.getOrDefault("retryAttempt",
MessageAttributeValue.builder().stringValue("0").build())
.stringValue()
);
try {
processMessage(sqsMessage.body());
// Successo: elimina dalla coda
sqsClient.deleteMessage(...);
} catch (TransientException e) {
if (currentAttempt >= MAX_ATTEMPTS) {
// Troppi tentativi: manda in DLQ manuale
sendToManualDLQ(sqsMessage, e);
sqsClient.deleteMessage(...);
return;
}
// Calcola delay esponenziale (1s, 2s, 4s, 8s, 16s...)
int delaySeconds = (int) Math.min(
Math.pow(2, currentAttempt),
MAX_DELAY_SECONDS
);
// Rimanda il messaggio con delay e contatore incrementato
sqsClient.sendMessage(
SendMessageRequest.builder()
.queueUrl(queueUrl)
.messageBody(sqsMessage.body())
.delaySeconds(delaySeconds)
.messageAttributes(Map.of(
"retryAttempt", MessageAttributeValue.builder()
.stringValue(String.valueOf(currentAttempt + 1))
.dataType("Number")
.build()
))
.build()
);
// Elimina il messaggio originale (non usare la DLQ automatica)
sqsClient.deleteMessage(...);
}
}
}
DLQ-monitoring: essentiële waarschuwingen
DLQ moet actief worden gemonitord. Een bericht in DLQ duidt op een reëel probleem dat vraagt om aandacht. De SQS-statistieken die u op CloudWatch moet controleren:
# CloudWatch Metric Alarms per DLQ - AWS CLI
# Alert: qualsiasi messaggio in DLQ (soglia 0)
aws cloudwatch put-metric-alarm \
--alarm-name "ordini-dlq-not-empty" \
--alarm-description "CRITICO: messaggi in DLQ ordini" \
--metric-name "ApproximateNumberOfMessagesVisible" \
--namespace "AWS/SQS" \
--dimensions Name=QueueName,Value=ordini-queue-dlq \
--period 60 \
--evaluation-periods 1 \
--statistic Sum \
--comparison-operator GreaterThanThreshold \
--threshold 0 \
--alarm-actions "arn:aws:sns:eu-west-1:123456:alerts-topic"
# Metriche importanti da monitorare su DLQ:
# - ApproximateNumberOfMessagesVisible: messaggi pronti per essere letti
# - ApproximateNumberOfMessagesNotVisible: messaggi in processing
# - NumberOfMessagesSent: rate di arrivo in DLQ (crescita = problema)
# - NumberOfMessagesDeleted: rate di reprocessing
Best practices voor DLQ in asynchrone systemen
- DLQ verplicht voor iedere consument: Er is geen asynchroon systeem in de productie zonder DLQ. Als ze ontbreken, gaan mislukte berichten verloren of blokkeren ze de stroom.
- Bewaak DLQ met nuldrempelwaarschuwingen: elk bericht in DLQ is een teken van een probleem. Wacht niet tot er honderden zijn verzameld voordat u reageert.
- Verrijk DLQ-berichten met metadata: kopteksten of attributen toevoegen met het fouttype, stacktrace, aantal nieuwe pogingen en tijdstempel van de fout. Zonder deze gegevens is het debuggen vrijwel onmogelijk.
- Lange retentie op de DLQ: configureer een retentie van 14 dagen (maximale SQS) of minstens 30 dagen op Kafka. Berichten in DLQ moeten beschikbaar zijn voor uitgestelde analyse.
- Test de herverwerking regelmatig: DLQ is nutteloos als je het niet weet hoe u berichten opnieuw kunt verwerken. Documenteer en test het herverwerkingsproces.
- Aparte fouttypen: Overweeg afzonderlijke DLQ's voor permanente fouten (corrupte payloads) en tijdelijke fouten (services uitgevallen). De herverwerkingsstrategie is anders.
Antipatroon: negeer de DLQ
Het gevaarlijkste patroon is om de DLQ te configureren en deze vervolgens niet te monitoren. De berichten stapelen zich wekenlang in stilte op, maar dan merkt iemand het op kritische gegevens ontbreken. Zet altijd een alert op de DLQ: het is het vangnet die je nooit mag negeren.
Volgende stappen in de serie
- Artikel 9 – Idempotentie bij consumenten: nieuwe pogingen en herverwerking vanuit DLQ kan dubbele berichten veroorzaken. Het idempotentie-sleutelpatroon is de belangrijkste verdediging.
- Artikel 10 – Outbox-patroon: zorg ervoor dat een evenement wordt gepubliceerd gebeurt altijd, zelfs in het geval van een producercrash, met behulp van een outbox-tabel in de database.
Link met andere series
- SQS versus SNS versus EventBridge (artikel 7): Elke AWS-service heeft zijn eigen service semantiek van DLQ en opnieuw proberen. In dit artikel worden de configuratieverschillen tussen de services besproken.
- Kafka Dead Letter Queue (artikel 10 van de Kafka-serie): het DLQ-patroon in Kafka met consumentengroep, onderwerp opnieuw proberen en opnieuw verwerken heeft dezelfde basis conceptueel maar een andere implementatie dan SQS.







