Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Operating Dead Letter Queues

This guide covers operational procedures for managing Dead Letter Queues in production Kafka environments. For conceptual background, see Dead Letter Queue Concepts. For implementation details, see Implementing DLQs.


Monitor DLQ topics for early detection of processing issues.

Dead letter queue monitoring metricsDead letter queue monitoring metricsDLQ Monitoring DashboardMessage RateQueue DepthAge DistributionError Breakdownmessages/min into DLQSpike = new failure patternTotal messages in DLQGrowth trend over timeOldest unprocessed messageMessages > 24h oldBy error classBy source topicBy consumer group
MetricSourceAlert Threshold
dlq.messages.in.rateProducer metrics> 10/min (baseline dependent)
dlq.messages.totalConsumer lag on DLQ> 1000 (application dependent)
dlq.oldest.message.ageCustom consumer> 24 hours
dlq.error.class.countHeader aggregationNew error class detected
dlq.source.topic.countHeader aggregationSpike from single topic
Terminal window
# Get DLQ topic message count
kafka-run-class.sh kafka.tools.JmxTool \
--object-name 'kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec,topic=orders.dlq' \
--jmx-url service:jmx:rmi:///jndi/rmi://broker1:9999/jmxrmi

Dead letter queue alert severity tiersDead letter queue alert severity tiersAlert Severity LevelsP1 - CriticalP2 - HighP3 - MediumP4 - InfoDLQ rate > 100/minNew poison error typeDLQ consumer stoppedDLQ rate > 10/minDLQ depth > 10,000Messages > 48h oldDLQ rate > 1/minDLQ depth > 1,000Messages > 24h oldAny DLQ messageDaily DLQ summary

Configure alerts based on the severity tiers above. Key alert conditions:

AlertConditionSeverity
DLQHighMessageRateDLQ ingestion rate > 10 messages/min sustained for 5 minutesHigh
DLQDepthCriticalUnprocessed DLQ messages > 10,000 for 10 minutesCritical
DLQConsumerStoppedDLQ processor consumption rate = 0 for 15 minutesCritical
DLQMessageAgeWarningOldest unprocessed message > 24 hoursMedium
DLQNewErrorTypePreviously unseen error class detectedHigh

For AxonOps alerting configuration, see Setup Alert Rules.


Dead letter queue triage workflowDead letter queue triage workflowDLQ alert triggeredCheck DLQ message rateSpike or gradual?spikegradualIdentify spike start timeCorrelate with deploymentsCheck producer changesLikely data quality issueSample recent messagesAnalyze error patternsGroup by error classGroup by source topicGroup by consumer groupIdentify root causeyesCode bug?Deploy fixReprocess DLQyesData issue?Fix upstream dataDecide: reprocess or discardyesTransient?unknownVerify recoveryAuto-reprocess viableEscalate to development
Terminal window
# Read recent DLQ messages with headers
kafka-console-consumer.sh \
--bootstrap-server broker1:9092 \
--topic orders.dlq \
--from-beginning \
--max-messages 10 \
--property print.headers=true \
--property print.timestamp=true \
--property print.key=true
# Filter by error type (requires header parsing)
kafka-console-consumer.sh \
--bootstrap-server broker1:9092 \
--topic orders.dlq \
--from-beginning \
--max-messages 100 \
--property print.headers=true | \
grep "dlq.error.class.*SerializationException"
#!/bin/bash
# analyze-dlq.sh - Analyze DLQ error distribution
DLQ_TOPIC=$1
BROKER=$2
SAMPLE_SIZE=${3:-1000}
echo "Analyzing $DLQ_TOPIC (sample: $SAMPLE_SIZE messages)"
# Extract error classes and count
kafka-console-consumer.sh \
--bootstrap-server $BROKER \
--topic $DLQ_TOPIC \
--from-beginning \
--max-messages $SAMPLE_SIZE \
--property print.headers=true 2>/dev/null | \
grep -oP 'dlq\.error\.class:\K[^,]+' | \
sort | uniq -c | sort -rn
echo ""
echo "Error distribution by source topic:"
kafka-console-consumer.sh \
--bootstrap-server $BROKER \
--topic $DLQ_TOPIC \
--from-beginning \
--max-messages $SAMPLE_SIZE \
--property print.headers=true 2>/dev/null | \
grep -oP 'dlq\.original\.topic:\K[^,]+' | \
sort | uniq -c | sort -rn

Decision framework for reprocessing dead letter queue messagesDecision framework for reprocessing dead letter queue messagesDLQ messages require reprocessingAssess message volume< 100 messages?yesnoManual review viableSelective reprocessingSame error class?yesnoBulk reprocessingAfter fix deployedCategorize by errorPrioritize by business impactChoose reprocessing methodyesOriginal topic?Messages reprocessedby original consumerReplay to source topicyesDirect processing?discardSpecialized handlerfor DLQ formatDLQ processor handlesUnrecoverable orno longer relevantArchive and delete
Replaying dead letter queue messages to the original topicDLQReplay ToolOriginal TopicConsumerDatabaseDLQDLQReplay ToolReplay ToolOriginal TopicOriginal TopicConsumerConsumerDatabaseDatabaseRead DLQ messagesStrip DLQ headersProduce to original topicNormal processingSuccessReplay process:- Reads from DLQ- Strips dlq.* headers- Sends to original topic- Tracks replayed offsets
#!/bin/bash
# replay-dlq.sh - Replay DLQ messages to original topic
DLQ_TOPIC=$1
BROKER=$2
MAX_MESSAGES=${3:-100}
# Create replay consumer group for tracking
GROUP_ID="dlq-replay-$(date +%s)"
echo "Replaying up to $MAX_MESSAGES messages from $DLQ_TOPIC"
echo "Consumer group: $GROUP_ID"
# Use kafkacat/kcat for replay (preserves headers, allows transformation)
kcat -C -b $BROKER -t $DLQ_TOPIC -G $GROUP_ID -c $MAX_MESSAGES -f '%h\n%k\n%s\n---\n' | \
while IFS= read -r headers && IFS= read -r key && IFS= read -r value && IFS= read -r sep; do
# Extract original topic from headers
original_topic=$(echo "$headers" | grep -oP 'dlq\.original\.topic=\K[^,]+')
if [ -n "$original_topic" ]; then
# Produce to original topic (without dlq headers)
echo "$value" | kcat -P -b $BROKER -t "$original_topic" -k "$key"
echo "Replayed to $original_topic: key=$key"
fi
done
@Service
public class DLQProcessor {
@KafkaListener(topics = "orders.dlq", groupId = "dlq-processor")
public void processDLQ(ConsumerRecord<String, byte[]> record) {
String errorClass = getHeader(record, "dlq.error.class");
String originalTopic = getHeader(record, "dlq.original.topic");
int retryCount = Integer.parseInt(getHeader(record, "dlq.retry.count"));
DLQAction action = determineAction(errorClass, retryCount);
switch (action) {
case REPLAY:
replayToOriginalTopic(record, originalTopic);
break;
case MANUAL_REVIEW:
storeForManualReview(record);
break;
case DISCARD:
logAndDiscard(record);
break;
}
}
private DLQAction determineAction(String errorClass, int retryCount) {
// Deserialization errors: likely need code fix, manual review
if (errorClass.contains("SerializationException")) {
return DLQAction.MANUAL_REVIEW;
}
// Transient errors that exceeded retry: try replay
if (errorClass.contains("TimeoutException") && retryCount < 10) {
return DLQAction.REPLAY;
}
// Validation errors: check if data was fixed upstream
if (errorClass.contains("ValidationException")) {
return DLQAction.REPLAY; // Will fail again if not fixed
}
return DLQAction.MANUAL_REVIEW;
}
}
Stages of a batch reprocessing jobStages of a batch reprocessing jobScheduled Batch Job1. Query DLQ2. Filter by criteria3. Transform messages4. Replay in batches5. Track progressProgressTableFilter criteria:- Error class- Age range- Source topic- Retry countBatch controls:- Rate limiting- Pause on errors- Progress checkpoints

Dead letter queue message lifecycle by ageDead letter queue message lifecycle by ageDLQ LifecycleActive(0-7 days)Review(7-30 days)Archive(30-90 days)Expired(> 90 days)Immediate investigationRapid reprocessingPending code fixesBatch reprocessingCompressed storageAudit complianceAuto-deleted by retentionAge > 7dAge > 30dAge > 90d
Terminal window
# Set DLQ retention to 30 days
kafka-configs.sh --bootstrap-server broker1:9092 \
--alter --entity-type topics --entity-name orders.dlq \
--add-config retention.ms=2592000000
# Set DLQ retention to 90 days for compliance
kafka-configs.sh --bootstrap-server broker1:9092 \
--alter --entity-type topics --entity-name payments.dlq \
--add-config retention.ms=7776000000
# Verify configuration
kafka-configs.sh --bootstrap-server broker1:9092 \
--describe --entity-type topics --entity-name orders.dlq
#!/bin/bash
# archive-dlq.sh - Archive old DLQ messages to cold storage
DLQ_TOPIC=$1
BROKER=$2
CUTOFF_DAYS=$3
S3_BUCKET=$4
CUTOFF_TS=$(($(date +%s) - ($CUTOFF_DAYS * 86400)))000
echo "Archiving messages older than $CUTOFF_DAYS days from $DLQ_TOPIC"
# Export old messages to file
kafka-console-consumer.sh \
--bootstrap-server $BROKER \
--topic $DLQ_TOPIC \
--from-beginning \
--property print.timestamp=true \
--property print.headers=true \
--property print.key=true \
--timeout-ms 30000 | \
awk -v cutoff="$CUTOFF_TS" '
/^CreateTime:/ {
ts = $2
if (ts < cutoff) { print; getline; print; getline; print }
}
' > /tmp/dlq-archive-$(date +%Y%m%d).json
# Compress and upload to S3
gzip /tmp/dlq-archive-$(date +%Y%m%d).json
aws s3 cp /tmp/dlq-archive-$(date +%Y%m%d).json.gz \
s3://$S3_BUCKET/dlq-archives/$DLQ_TOPIC/
echo "Archived to s3://$S3_BUCKET/dlq-archives/$DLQ_TOPIC/"

Runbook for a dead letter queue rate spikeRunbook for a dead letter queue rate spikeReceive DLQ spike alertCheck DLQ dashboardNote: spike start time,rate, error typesSample 10 recent DLQ messagesExtract error class distributionIdentify source topic(s)Single error type?yesnoLikely code bug orupstream data changeMultiple failure modesPrioritize by volumeCheck recent deploymentsCheck upstream system changesyesCode bug identified?Create hotfixDeploy fixMonitor DLQ rateRate normalized?yesnoPlan DLQ reprocessingEscalate to developmentyesUpstream data issue?transientContact upstream teamWait for data fixReprocess after fix confirmedMonitor for recoveryAuto-reprocess if stableDocument incidentUpdate runbook if neededClose alertOperationsInvestigationResolutionClosure
Terminal window
# Check DLQ depth
kafka-consumer-groups.sh --bootstrap-server broker1:9092 \
--describe --group dlq-processor | grep "orders.dlq"
# Get DLQ message count
kafka-run-class.sh kafka.tools.GetOffsetShell \
--broker-list broker1:9092 \
--topic orders.dlq --time -1
# Sample recent errors
kafka-console-consumer.sh --bootstrap-server broker1:9092 \
--topic orders.dlq --from-beginning --max-messages 5 \
--property print.headers=true
# Pause DLQ consumer (for investigation)
kafka-consumer-groups.sh --bootstrap-server broker1:9092 \
--group dlq-processor --topic orders.dlq \
--reset-offsets --to-current --execute
# Count messages by error type (last 1000)
kafka-console-consumer.sh --bootstrap-server broker1:9092 \
--topic orders.dlq --from-beginning --max-messages 1000 \
--property print.headers=true 2>/dev/null | \
grep -oP 'dlq\.error\.class:\K[^\s,]+' | sort | uniq -c | sort -rn

FactorConsideration
Expected error rate0.1-1% of main topic volume typical
Retention period30-90 days for investigation window
Message sizeSame as source + ~500 bytes headers
Replication factorMatch or exceed source topic RF
Partition count1-3 sufficient for most DLQs
DLQ Storage = Main Topic Volume × Error Rate × Retention Days × RF
Example:
- Main topic: 100 GB/day
- Error rate: 0.5%
- Retention: 30 days
- RF: 3
DLQ Storage = 100 GB × 0.005 × 30 × 3 = 45 GB