TL;DR
- Error types: Deserialization errors (skip), Transient errors (retry), Permanent errors (DLT)
- Use @RetryableTopic for declarative retry and DLT handling (Spring Kafka 2.7+)
- DefaultErrorHandler + DeadLetterPublishingRecoverer for DLT publishing
- Handle deserialization errors with ErrorHandlingDeserializer
- Set up alerts when DLT messages arrive for quick response
Target Audience: Developers implementing Consumer error handling strategies
Prerequisites: Consumer operation principles from Consumer Group & Offset, Spring Kafka basics
Understand Kafka Consumer error handling strategies and Dead Letter Topic patterns.
Error Types#
Errors occurring in Kafka Consumer are broadly classified into three types.
flowchart TB
subgraph Errors["Error Types"]
DESER["Deserialization Errors<br>(Message format issues)"]
TRANS["Transient Errors<br>(Network, DB connection)"]
PERM["Permanent Errors<br>(Business logic failure)"]
end
DESER --> SKIP["Skip/DLT"]
TRANS --> RETRY["Retry"]
PERM --> DLT["Dead Letter Topic"]Diagram: Handling strategy by error type - Deserialization errors are skipped/DLT, transient errors are retried, permanent errors are sent to DLT.
Deserialization errors are message format issues like JSON parsing failure.
Key PointsSince the message itself is wrong, retrying won’t solve it. Skip or send to Dead Letter Topic.
- Deserialization errors: Message format issues, retry is pointless → skip/DLT
- Transient errors: DB connection, timeout, etc. → can be resolved with retry
- Permanent errors: Business logic failure → send to DLT for separate handling
Transient errors are temporary issues like DB connection failures or timeouts. Since retrying after a short wait has high success probability, apply retry strategy.
Permanent errors are business logic issues like validation failures. Since retrying produces the same result, send to Dead Letter Topic for separate handling.
Basic Error Handling#
DefaultErrorHandler (Spring Kafka 2.8+)
The default error handler provided since Spring Kafka 2.8. Retries at configured intervals and count, and skips the record after maximum retry count is exceeded.
@Configuration
public class KafkaConfig {
@Bean
public DefaultErrorHandler errorHandler() {
// 3 retries at 1 second intervals
return new DefaultErrorHandler(
new FixedBackOff(1000L, 3L)
);
}
}Retry Strategies
FixedBackOff retries at fixed intervals. The example above retries 3 times at 1 second intervals.
ExponentialBackOff increases wait time exponentially. First retry after 1 second, second after 2 seconds, third after 4 seconds, and so on.
@Bean
public DefaultErrorHandler errorHandler() {
ExponentialBackOff backOff = new ExponentialBackOff(1000L, 2.0);
backOff.setMaxElapsedTime(60000L); // Max 1 minute
return new DefaultErrorHandler(backOff);
}Dead Letter Topic (DLT)#
Dead Letter Topic is a separate Topic that stores messages that cannot be processed after retries. By not discarding failed messages but keeping them, you can analyze or manually process them later.
flowchart LR
subgraph Main["Main Topic"]
MSG[Message]
end
subgraph Consumer["Consumer"]
PROC[Process]
RETRY[Retry]
end
subgraph DLT["Dead Letter Topic"]
DEAD[Failed Message]
end
MSG --> PROC
PROC -->|Fail| RETRY
RETRY -->|Max retries exceeded| DEAD
RETRY -->|Success| DONE[Done]Diagram: DLT processing flow - On message processing failure, retry, send to Dead Letter Topic when max retries exceeded.
Key Points
- Dead Letter Topic: Storage space for messages that failed after retries
- Failed messages are kept (not discarded) for analysis/manual processing
- Default DLT Topic name: original-topic.DLT (e.g., orders.DLT)
DeadLetterPublishingRecoverer
DLT publishing feature provided by Spring Kafka. Sends messages to DLT when they fail after maximum retries. Default DLT Topic name is original-topic.DLT (e.g., orders.DLT).
@Configuration
public class KafkaConfig {
@Bean
public DefaultErrorHandler errorHandler(
KafkaTemplate<String, Object> kafkaTemplate) {
DeadLetterPublishingRecoverer recoverer =
new DeadLetterPublishingRecoverer(kafkaTemplate);
return new DefaultErrorHandler(
recoverer,
new FixedBackOff(1000L, 3L)
);
}
}DLT Customization
You can change the DLT Topic name or configure certain exceptions to not retry.
@Bean
public DefaultErrorHandler errorHandler(
KafkaTemplate<String, Object> kafkaTemplate) {
// Customize DLT Topic name
DeadLetterPublishingRecoverer recoverer =
new DeadLetterPublishingRecoverer(kafkaTemplate,
(record, exception) ->
new TopicPartition(
record.topic() + "-dead-letter",
record.partition()
));
// Don't retry certain exceptions
DefaultErrorHandler handler = new DefaultErrorHandler(
recoverer,
new FixedBackOff(1000L, 3L)
);
handler.addNotRetryableExceptions(
ValidationException.class,
NullPointerException.class
);
return handler;
}ValidationException or NullPointerException produce the same result on retry, so send immediately to DLT.
@RetryableTopic (Recommended)#
Declarative retry and DLT handling provided in Spring Kafka 2.7+. Define retry policy with annotations for cleaner code.
Basic Usage
@Component
public class OrderConsumer {
@RetryableTopic(
attempts = "4", // 1 original + 3 retries
backoff = @Backoff(delay = 1000, multiplier = 2)
)
@KafkaListener(topics = "orders")
public void consume(OrderEvent event) {
// Processing logic
processOrder(event);
}
@DltHandler
public void handleDlt(OrderEvent event,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
@Header(KafkaHeaders.EXCEPTION_MESSAGE) String error) {
log.error("DLT received - Topic: {}, Error: {}", topic, error);
// DLT message processing (alerts, logging, etc.)
alertService.sendAlert(event, error);
}
}Retry Topic Structure
@RetryableTopic automatically creates retry Topics. On failure from original orders, it goes through orders-retry-0, orders-retry-1, orders-retry-2, and finally to orders-dlt.
flowchart LR
ORIG["orders"] -->|Fail| R0["orders-retry-0"]
R0 -->|Fail| R1["orders-retry-1"]
R1 -->|Fail| R2["orders-retry-2"]
R2 -->|Fail| DLT["orders-dlt"]
R0 -->|Success| DONE1[Done]
R1 -->|Success| DONE2[Done]
R2 -->|Success| DONE3[Done]Diagram: @RetryableTopic retry flow - On failure from orders, goes through orders-retry-0, retry-1, retry-2, and finally to orders-dlt.
Key Points
- @RetryableTopic: Declarative retry and DLT handling (Spring Kafka 2.7+)
- Auto-creates retry Topics: topic-retry-0, retry-1, …, topic-dlt
- Implement DLT message handling logic with @DltHandler
Advanced Settings
@RetryableTopic(
attempts = "4",
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000),
autoCreateTopics = "true",
topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE,
dltStrategy = DltStrategy.ALWAYS_RETRY_ON_ERROR,
include = {RetryableException.class}, // Exceptions to retry
exclude = {NonRetryableException.class} // Exceptions not to retry
)
@KafkaListener(topics = "orders")
public void consume(OrderEvent event) {
// ...
}attempts is total attempt count (default 3). backoff.delay is base wait time (default 1000ms), backoff.multiplier is wait time increase rate (default 0 for fixed), backoff.maxDelay is maximum wait time. include specifies exceptions to retry, exclude specifies exceptions not to retry.
Deserialization Error Handling#
When message deserialization fails, Consumer cannot process that message. Using ErrorHandlingDeserializer allows handling deserialization failures in the application.
spring:
kafka:
consumer:
key-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
properties:
spring.deserializer.key.delegate.class: org.apache.kafka.common.serialization.StringDeserializer
spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer
spring.json.trusted.packages: "com.example.*"On deserialization failure, value is passed as null, and exception information is included in headers.
@KafkaListener(topics = "orders")
public void consume(ConsumerRecord<String, OrderEvent> record) {
if (record.value() == null) {
// Deserialization failed
log.error("Deserialization failed: {}", new String(record.headers()
.lastHeader("springDeserializerExceptionValue").value()));
return;
}
processOrder(record.value());
}Error Handling Patterns#
Pattern 1: Retry + DLT
The most common pattern. Retry a certain number of times, then send to DLT on failure.
@RetryableTopic(attempts = "4")
@KafkaListener(topics = "orders")
public void consume(OrderEvent event) {
validateAndProcess(event);
}
@DltHandler
public void handleDlt(OrderEvent event) {
saveToFailedOrders(event);
notifyAdmin(event);
}Pattern 2: Conditional Retry
Decide whether to retry based on exception type. Retry transient errors, log and skip permanent errors.
@KafkaListener(topics = "orders")
public void consume(OrderEvent event) {
try {
processOrder(event);
} catch (TemporaryException e) {
// Can retry → throw exception
throw e;
} catch (PermanentException e) {
// Can't retry → log and skip
log.error("Cannot process: {}", event, e);
saveToFailedOrders(event, e);
}
}Pattern 3: Manual Reprocessing
Read messages from DLT, manually review, and reprocess.
@KafkaListener(topics = "orders-dlt")
public void processDlt(
ConsumerRecord<String, OrderEvent> record,
@Header(KafkaHeaders.EXCEPTION_MESSAGE) String error) {
OrderEvent event = record.value();
// Reprocess after manual review
if (canBeFixed(event)) {
OrderEvent fixed = fixEvent(event);
kafkaTemplate.send("orders", record.key(), fixed);
log.info("Reprocessing complete: {}", record.key());
} else {
permanentlyFailed(event, error);
}
}Monitoring and Alerts#
Send alerts immediately when messages arrive in DLT for quick response.
@DltHandler
public void handleDlt(
ConsumerRecord<String, OrderEvent> record,
@Header(KafkaHeaders.EXCEPTION_MESSAGE) String error,
@Header(KafkaHeaders.ORIGINAL_TOPIC) String originalTopic,
@Header(KafkaHeaders.ORIGINAL_OFFSET) long originalOffset) {
DltMessage dltMessage = DltMessage.builder()
.key(record.key())
.value(record.value())
.originalTopic(originalTopic)
.originalOffset(originalOffset)
.error(error)
.timestamp(Instant.now())
.build();
// Slack/email alert
alertService.sendDltAlert(dltMessage);
// Record metrics
meterRegistry.counter("kafka.dlt.received",
"topic", originalTopic).increment();
}Summary#
For error handling strategies, @RetryableTopic provides declarative retry, DefaultErrorHandler provides programmatic handling, @DltHandler provides DLT handling.
By error type, retry transient errors with exponential backoff, move permanent errors immediately to DLT, log and skip deserialization errors. Send alerts and manually review messages stored in DLT.
Next Steps#
- Monitoring Basics - Kafka monitoring and metrics