Kafka Dead Letter Queue: Spring, Connect & Streams
A Kafka dead letter queue (DLQ) is a topic for records an application could not process. It is also called a dead-letter topic (DLT). Kafka's consumer API does not provide one.
A record that keeps failing is often called a poison pill. Retrying it over and over blocks the records behind it in the same partition. Whether it reaches the DLQ, gets skipped, or stops the application depends on the framework and its settings.
This article compares DLQ behavior in Spring Kafka, Spring Cloud Stream, Kafka Connect, and Kafka Streams. It covers retry and topic settings, then shows how to diagnose failures, replay records, and verify that they were processed successfully.
Kafka dead letter queues compared
Behavior verified against each release's documentation and source: 21 Oct 2026
| Spring Kafka 4.1 | Spring Cloud Stream 5.0.3 | Kafka Connect 4.3.1 | Kafka Streams 4.3.1 | |
|---|---|---|---|---|
| Errors that reach the DLQ | Listener errors after retries. Deserialization errors only with ErrorHandlingDeserializer. | Handler errors after retries | Converter and transform errors. Connector errors only if the connector reports them. | Deserialization, processing, and production errors. Global state store updates are excluded. |
| How to enable | DefaultErrorHandler with DeadLetterPublishingRecoverer | enableDlq on the consumer binding | errors.tolerance=all and errors.deadletterqueue.topic.name | errors.dead.letter.queue.topic.name |
| Retries before the DLQ | DefaultErrorHandler retries retryable errors 9 times by default. @RetryableTopic makes 3 attempts in total. | 3 attempts in total | None by default | None. The handler skips the record or stops the application. |
| Default topic name | <topic>-dlt | error.<topic>.<group> | None, you set it | None, you set it |
| DLQ partition | Same as the source, if the DLT has that partition | Same as the source record | Chosen by the producer | Chosen by the producer |
| Error headers | kafka_dlt-* | x-exception-* and x-original-* | __connect.errors.*, off by default | __streams.errors.* |
| Original bytes kept | Only for deserialization failures | Yes, by default | Yes | Yes. Errors in punctuations (scheduled callbacks) and asynchronous sends have no key or value. |
| If the DLQ write fails | Throws, and the record is redelivered | Only logged, and the record is skipped | Logged. For converter and transform errors, the record is skipped anyway. | Treated as a send error, which stops the instance by default |
Plain Kafka consumers have no DLQ. See Plain Kafka consumers to build one.
Framework defaults and DLQ limitations
Spring Kafka dead letter queue
By default, Spring Kafka's DefaultErrorHandler logs a failed record and skips it once its retries run out. To send the record to a DLT instead, give the handler a DeadLetterPublishingRecoverer.
Records that fail deserialization never reach the handler, because they fail inside the consumer's poll. The consumer then keeps failing at the same offset. Wrap the deserializer in ErrorHandlingDeserializer to send them straight to the DLT with their original bytes.
Spring Cloud Stream's Kafka binder has its own DLQ, turned on with enableDlq on the consumer binding. If the DLQ write fails, the binder only logs the error and skips the record. Check the application logs for DLQ send failures.
Kafka Connect dead letter queue
By default, a Kafka Connect task stops on the first record it cannot convert. To send those records to a DLQ and keep the task running, set errors.tolerance=all and errors.deadletterqueue.topic.name. Only sink connectors have a DLQ.
This covers converters and transforms (SMTs). Errors in the connector's own code, such as a failed database write, reach the DLQ only if the connector reports them. Check the connector's documentation for which failures it reports.
DLQ records carry no error reason by default. Set errors.deadletterqueue.context.headers.enable=true to add the exception and the source topic, partition, and offset as headers.
Kafka Streams dead letter queue
From Kafka 4.2, Kafka Streams can write failed records to a DLQ. Setting errors.dead.letter.queue.topic.name makes the built-in exception handlers send each failed record to that topic.
The default handlers still stop the application when a record fails. To skip such records and keep processing, set deserialization.exception.handler to LogAndContinueExceptionHandler and processing.exception.handler to LogAndContinueProcessingExceptionHandler.
These handlers are in the org.apache.kafka.streams.errors package and support the DLQ setting. A custom handler ignores it unless the handler returns DLQ records itself.
Plain Kafka consumers
A plain consumer has no DLQ, so you build one. When processing fails, produce the record to a DLT with the source topic, partition, offset, and exception as headers, then commit the source offset.
Commit only after the DLT write succeeds. The send is asynchronous, so committing before it completes, by hand or through auto commit, loses the record if the write fails. Set enable.auto.commit=false and wait for the send result before committing.
Some client libraries add a DLQ for you, so check how they handle a failed DLQ write. Laravel Kafka, for example, ignores it. See Kafka clients for your language.
Set up retries and a Kafka DLQ topic
Retry or send to the DLQ
Retrying malformed data does not fix it. Send those records to the DLQ. Retry temporary failures, such as timeouts or an unavailable downstream service.
In Spring Kafka, DefaultErrorHandler already skips retries for deserialization and message conversion errors and sends them straight to the DLT. Register the exceptions your own code throws for bad data with addNotRetryableExceptions.
Retrying a record over and over blocks the records behind it until the retries and the delays between them run out. A retry topic, such as Spring's @RetryableTopic, moves it aside so others keep flowing, but records with the same key may then be processed out of order. Use one only when that order does not matter.
DLT partitions, retention, and cleanup
DLT records can expire before you fix the problem. Kafka's default retention is seven days. Check the topic's effective retention.ms and allow enough time to investigate, fix, and replay.
- Partitions: If you create a Spring Cloud Stream DLQ topic yourself, give it at least as many partitions as the source. The binder writes each record to the same partition number, and if that partition is missing, the write fails and is only logged.
- Cleanup policy: Use
cleanup.policy=delete. Compaction keeps only the latest record per key, so earlier failures disappear. - Replication: Kafka Connect creates a missing DLQ topic with one partition and three replicas by default. On a one-broker development cluster, set
errors.deadletterqueue.topic.replication.factor=1. - Permissions: With ACLs on, the application needs Write on the DLT.
In Kafma, a desktop Kafka IDE, use Edit Configuration to check the DLT's retention and cleanup policy, and Add Partitions to increase its partition count. Use the ACL Simulator to see which ACL rules allow or block the application's writes.

Inspect and replay records from a Kafka DLQ
A record in the DLQ was skipped, so whatever it was meant to do, such as saving an order to the database, may not have finished. Once you fix the cause, replay the record: send it back to the original topic so the consumer processes it again.
Diagnose the failure
Start by grouping failures. Read the exception and source topic headers on several records and check whether they share one cause. Then decide whether the failure comes from bad data, such as a producer writing the wrong format, or from a bug in your code.
In Kafma, expand a DLT record to inspect its error headers. Switch between Decoded and Raw to check the stored payload and its encoding against what the consumer expects.

Prepare records for replay
The fix depends on what went wrong. If it was your code or configuration, such as a converter setting, deploy the fix and replay the records unchanged. If the data itself is wrong, correct it before replaying, or leave records you no longer need in the DLQ.
Before you replay, check that the key and value use the encoding the consumer expects. Keep the headers the consumer needs and the source topic, partition, and offset for tracing. Remove the exception details, which are stale after the fix.
The record may have had side effects before it failed, so make sure the consumer can safely process it twice. Replay one record first. If it lands in the DLQ again, stop and diagnose the new failure before replaying more.
Replay and verify the result
In Kafma, select Replay to topic on the DLT record and choose the original topic. Review the draft using the checks above. Partition starts as Auto, which may pick another partition, so set the source partition from the error headers before selecting Produce.

The replayed record gets a new offset, which Kafma shows after the send. Open Watch Group on the consumer group, such as connect-<connector name> for a sink connector, and wait until Kafma marks the record as consumed.
A committed offset only means the consumer moved on, so also check the expected result, such as the output record or the database row.

Download Kafma and connect to the cluster with your DLQ.
Frequently asked questions
Does Kafka have a dead letter queue?
Not in the consumer API. Kafka Connect and Kafka Streams, both part of Apache Kafka, have one, and Spring Kafka adds its own. Share groups, Kafka's queue-style consumers, have no DLQ in 4.3.1 either. They archive a record after its last delivery attempt.
What is a poison pill in Kafka?
A poison pill is a record that repeatedly fails processing, often because of bad data or an unreadable format. Retrying it over and over can block the records behind it in the same partition.
What is the difference between a DLQ and a retry topic?
A retry topic holds records for another attempt after a delay. A DLQ holds records that were not retried or ran out of retries, so inspect the cause before replaying them. Spring Kafka's @RetryableTopic uses both. See Retry or send to the DLQ.
Can records be lost even with a DLQ?
Yes, if the DLQ write fails and the consumer moves on anyway. Spring Cloud Stream only logs a failed DLQ write, and Kafka Connect can skip the record. Plain consumers lose records if they commit before the DLT write succeeds.
Should each topic have its own DLQ?
Usually, yes. One DLQ per source topic keeps replay simple, since every record goes back to one topic. Spring Kafka and Spring Cloud Stream default to this. A shared DLQ also works, but replay then relies on the source topic header.
Do Kafka Connect source connectors have a DLQ?
No. Kafka Connect has no built-in DLQ for source connectors. With errors.tolerance=all, a source connector skips records that fail conversion or a transform, and logs them when errors.log.enable=true.
Conclusion
Each framework sends different failures to its DLQ, and in some of them a failed DLQ write still loses the record. Check your framework's column in the comparison table before you rely on its DLQ.
Retry only temporary errors, send bad data straight to the DLQ, and keep DLT records long enough to investigate and fix. When you replay, start with one record, and check both the consumer's offset and the expected result before replaying the rest.
This guide is maintained by the team behind Kafma.