Skip to content

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

Dead Letter Queues

A Dead Letter Queue (DLQ) is a separate topic that stores messages which cannot be processed successfully. Rather than blocking the consumer, losing the message, or retrying indefinitely, failed messages are moved to the DLQ for later analysis and reprocessing.

Kafka does not provide built-in DLQ functionality—it is an application-level pattern that must be implemented by producers or consumers. This follows Kafka’s “dumb broker, smart consumer” architecture where error handling responsibility lies with client applications rather than the broker.


Traditional message brokers (JMS, RabbitMQ, IBM MQ) provide built-in DLQ functionality, typically routing messages based on:

  • TTL expiration - message exceeded time-to-live
  • Delivery failures - maximum delivery attempts exceeded
  • Queue capacity - destination queue full

Kafka DLQs serve a different purpose. Since Kafka retains messages regardless of consumption and consumers control their own offsets, DLQs in Kafka primarily address:

  • Invalid message format - deserialization failures, schema mismatches
  • Bad message content - validation errors, missing required fields
  • Processing failures - business logic exceptions, dependency errors

This distinction is important: Kafka DLQs are about message quality, not delivery mechanics.


A poison message is a message that causes consumer failure repeatedly. Without proper handling, a single poison message can halt an entire consumer group.

ProducerordersConsumerDatabaseProducerProducerordersordersConsumerConsumerDatabaseDatabaseOrder messageDeliver messageParse messageFAILDeserialization failsDon't commit offsetRedeliver same messageFAILFails againInfinite loop: consumer stuckon poison message forever.No progress on partition.
CauseDescription
Schema mismatchProducer schema incompatible with consumer’s deserializer
Corrupt dataMalformed JSON, invalid Avro, truncated payload
Business validationData fails domain validation (negative price, invalid date)
Missing dependenciesReferenced entity doesn’t exist in database
Transient failuresDatabase timeout, network error (may succeed on retry)
Code bugsConsumer code throws exception for certain data patterns

The DLQ pattern isolates failed messages so healthy messages continue processing.

ProducerordersConsumerorders.dlqDatabaseProducerProducerordersordersConsumerConsumerorders.dlqorders.dlqDatabaseDatabaseOrder messageDeliver messageParse messageFAILProcessing failsSend to DLQ(with error metadata)Commit offset(move past poison message)Next messageProcess successfullyCommit offsetDLQ contains:- Original message- Error details- Retry count- Timestamp
BenefitDescription
Fault isolationOne bad message doesn’t block partition processing
No data lossFailed messages preserved for analysis and reprocessing
VisibilityDLQ depth indicates system health issues
DebuggingFailed messages available for root cause analysis
Controlled retryMessages can be reprocessed after fixes deployed

Production FlowError FlowordersOrderConsumerOrder DBorders.dlqDLQProcessorAlertServiceDLQ naming convention:{original-topic}.dlqor{original-topic}.dead-letterNormal processingSuccessFailureAnalyze failuresTrigger alertsReprocess(after fix)

For transient failures, implement multiple retry stages before final DLQ:

ordersorders.retry-1orders.retry-2orders.dlqConsumerRetry topics use delayedconsumption or time-basedpartition assignmentDLQ: requires manualintervention or code fixProcessFail (attempt 1)Retry after 1 minFail (attempt 2)Retry after 5 minFail (attempt 3)Permanent failure
StrategyImplementationUse Case
Immediate retryConsumer retries N times in-memoryTransient network glitches
Delayed retrySeparate retry topics with consumer pauseRate limiting, backpressure
Exponential backoffIncreasing delays (1s, 5s, 30s, 5m)External service recovery
Scheduled retryTime-windowed reprocessingBatch reconciliation

Organizations must decide between dedicated DLQs per topic or a unified DLQ.

StrategyApproachTrade-offs
Per-topic DLQorders.dlq, payments.dlq, users.dlqTargeted analysis, clear ownership; more topics to manage
Unified DLQSingle application.dlq for all topicsSimpler operations, single dashboard; harder root cause analysis
Per-service DLQorder-service.dlq handles multiple input topicsBalanced approach; requires header-based routing

Most production deployments use per-topic DLQs for clear ownership and targeted alerting.

DLQ messages can remain in Kafka or be moved to external storage for long-term retention and analysis.

Kafka-NativeHybridExternalorders.dlqPros: Simple, same toolingCons: Kafka storage costsorders.dlqS3 ArchivePros: Cost-effective long-termCons: Reprocessing complexityordersPostgreSQLDLQ TableReview UIPros: SQL queries, UI toolingCons: Operational complexityArchive after 7 daysOn failureManual review
StorageBest ForConsiderations
Kafka topicStandard use cases, automated reprocessingSet appropriate retention; monitor disk usage
S3/GCS archiveCompliance, long-term retentionBatch reprocessing; requires ETL tooling
Database (PostgreSQL)Manual review workflows, complex remediationEnables UI/CLI tooling; additional infrastructure

A DLQ message should contain the original message plus metadata for debugging and reprocessing.

DLQ MessageHeadersKeyValuedlq.original.topic: ordersdlq.original.partition: 3dlq.original.offset: 12847dlq.original.timestamp: 1705...dlq.error.message: NPE at...dlq.error.class: NullPointer...dlq.retry.count: 3dlq.consumer.group: order-svcorder-123Original message payload(unchanged)
HeaderPurpose
dlq.original.topicSource topic name
dlq.original.partitionSource partition
dlq.original.offsetSource offset (for replay tracking)
dlq.original.timestampOriginal message timestamp
dlq.original.keyOriginal message key (if key changed)
dlq.error.messageException message
dlq.error.classException class name
dlq.error.stacktraceStack trace (optional, can be large)
dlq.retry.countNumber of processing attempts
dlq.failed.timestampWhen message was sent to DLQ
dlq.consumer.groupConsumer group that failed
dlq.consumer.instanceSpecific consumer instance

Not all errors should go to the DLQ. Classify errors to determine the appropriate handling strategy.

Message processingfailsTransient error?yesnoRetry with backoffMax retries exceeded?yesnoSend to DLQRetryRecoverable with fix?yesnoCode fix needed.Reprocess later.Send to DLQData corruption?yesnoLog and alert.Manual cleanup.Send to DLQTruly unprocessable.No value in keeping.Log and skip
CategoryExamplesStrategy
TransientConnection timeout, rate limit, lock contentionRetry with backoff
RecoverableSchema mismatch, validation failure, missing referenceDLQ → fix → reprocess
CorruptInvalid encoding, truncated message, wrong topicDLQ → investigate → discard
PoisonCauses crash/OOM, infinite loop triggerDLQ → immediate alert

DLQ topics should be configured for durability and long retention since messages may need reprocessing weeks later.

# DLQ topic configuration
cleanup.policy=delete
retention.ms=2592000000 # 30 days (longer than main topics)
retention.bytes=-1 # No size limit
min.insync.replicas=2 # Durability
replication.factor=3 # Durability
compression.type=producer # Preserve original compression
StrategyApproachTrade-off
Single partitionAll DLQ messages in one partitionSimple, ordered review; limited throughput
Match sourceSame partition count as source topicPreserves key locality; complex reprocessing
By error typePartition by error categoryEasy triage; custom partitioner needed
Round-robinDefault partitioningBalanced load; no ordering

Error Handling ApproachesRetry In-PlaceSkip and LogDead Letter QueueParking LotPros: SimpleCons: Blocks partitionPros: No blockingCons: Data lossPros: No blocking,no data lossCons: Complexity,extra topicsPros: Human reviewCons: Manual process
ApproachBlockingData LossComplexityBest For
Retry in-placeYesNoLowTransient errors only
Skip and logNoYesLowNon-critical data
DLQNoNoMediumProduction systems
Parking lotNoNoHighCompliance, finance

The best DLQ strategy is minimizing messages that reach it. Schema Registry provides producer-side validation that catches bad messages before they enter Kafka.

ProducerSchema RegistryordersConsumerorders.dlqProducerProducerSchema RegistrySchema RegistryordersordersConsumerConsumerorders.dlqorders.dlqValidate schemaalt[Schema valid]OKSend messageConsumeProcess successfully[Schema invalid]RejectHandle errorat sourceBad message neverenters Kafka.No DLQ needed.
StrategyImplementationEffectiveness
Schema RegistryEnforce Avro/Protobuf/JSON Schema at producerCatches format errors before Kafka
Producer validationValidate business rules before sendCatches domain errors at source
Contract testingVerify producer/consumer compatibility in CICatches schema drift before deployment
Input sanitizationClean/normalize data at ingestion boundaryReduces malformed data

Prevention reduces DLQ volume but cannot eliminate it entirely—runtime failures, dependency issues, and edge cases still require DLQ handling.


  • Order processing where every message must be accounted for
  • Financial transactions requiring audit trails
  • Event sourcing where event loss corrupts state
  • Multi-tenant systems where one tenant’s bad data shouldn’t affect others
  • Integration pipelines where upstream data quality varies
  • Metrics/telemetry where occasional loss is acceptable
  • Cache invalidation events (stale cache self-corrects)
  • Heartbeats/health checks
  • High-volume logs where DLQ would be overwhelmed

Anti-Pattern: DLQ as Primary Error Handling

Section titled “Anti-Pattern: DLQ as Primary Error Handling”
Anti-PatternCorrect PatternordersConsumer(no retry logic)orders.dlqordersConsumer(retry + classify)orders.retryorders.dlqAny errorgoes to DLQTransienterrorsPermanentfailures only
Anti-PatternProblemSolution
No retry before DLQTransient errors flood DLQImplement retry with backoff
DLQ without monitoringSilent failures accumulateAlert on DLQ depth
No reprocessing planDLQ becomes data graveyardBuild reprocessing tooling
Infinite retryPoison messages never reach DLQSet max retry limit
Losing error contextCan’t debug failuresInclude error metadata in headers
Same retention as sourceDLQ expires before reviewLonger DLQ retention
DLQ for backpressureUsing DLQ to handle load spikesScale consumers or use quotas
Connection errors to DLQNetwork timeouts sent to DLQRetry in application; fix connectivity
No DLQ ownershipNobody reviews DLQ messagesAssign data owners, not just infrastructure
Ignoring DLQ entirelyMessages accumulate indefinitelyProcess or archive with defined SLA

Effective DLQ management requires clear ownership and defined processes.

RoleResponsibility
Data ownerReview failed messages, determine if data fix needed
Development teamFix code bugs causing failures, deploy fixes
OperationsMonitor DLQ depth, trigger alerts, manage retention
Platform teamProvide reprocessing tooling, maintain DLQ infrastructure
  • SLA for review - define maximum time messages can remain in DLQ unreviewed
  • Escalation path - who gets notified when DLQ depth exceeds thresholds
  • Reprocessing authority - who can trigger replay of DLQ messages
  • Discard policy - criteria for permanently discarding unrecoverable messages