Spring Kafka: Error Handling & Retry
Spring Kafka provides several error handling strategies, from simple logging to automatic retry with backoff and dead letter topics, so that transient failures don't cause message loss.
Contents
Default Error Handler
Since Spring Kafka 2.8, DefaultErrorHandler
// This is wired automatically — you don't need to declare it unless customising
import org.springframework.kafka.listener.DefaultErrorHandler;
// The legacy SeekToCurrentErrorHandler (pre-2.8) behaved similarly
// but DefaultErrorHandler supersedes it and supports BackOff natively
Fixed & Exponential Backoff Retry
Configure FixedBackOffExponentialBackOff
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.util.backoff.ExponentialBackOff;
import org.springframework.util.backoff.FixedBackOff;
// Fixed: retry up to 3 times with a 2-second pause between attempts
DefaultErrorHandler fixedHandler = new DefaultErrorHandler(
new FixedBackOff(2000L, 3L) // interval ms, maxAttempts
);
// Exponential: start at 1s, double each time, cap at 30s, up to 5 attempts
ExponentialBackOff expBackOff = new ExponentialBackOff(1000L, 2.0);
expBackOff.setMaxInterval(30_000L);
expBackOff.setMaxElapsedTime(120_000L); // stop after 2 minutes total
DefaultErrorHandler expHandler = new DefaultErrorHandler(expBackOff);
max.poll.interval.ms@RetryableTopic@RetryableTopic
@RetryableTopicorders-retry-1orders-retry-2
import org.springframework.kafka.annotation.DltHandler;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.RetryableTopic;
import org.springframework.kafka.retrytopic.TopicSuffixingStrategy;
import org.springframework.retry.annotation.Backoff;
import org.springframework.stereotype.Service;
@Service
public class OrderConsumer {
@RetryableTopic(
attempts = "4", // 1 original + 3 retries
backoff = @Backoff(delay = 1000, multiplier = 2.0, maxDelay = 10000),
autoCreateTopics = "true", // auto-create retry + DLT topics
topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_DELAY_VALUE
)
@KafkaListener(topics = "orders")
public void consume(String message) {
System.out.println("Processing: " + message);
if (message.contains("BAD")) {
throw new RuntimeException("Simulated processing failure");
}
}
@DltHandler
public void handleDlt(String message) {
System.err.println("DLT received: " + message);
// save to error store, alert, or re-queue manually
}
}
@RetryableTopic@EnableKafkaRetryTopic@Configuration@EnableKafkaDead Letter Topic
After all retry attempts are exhausted, messages that still fail are routed to a Dead Letter Topic (DLT). Annotate a method with @DltHandlerDeadLetterPublishingRecoverer
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.backoff.FixedBackOff;
@Configuration
public class KafkaErrorConfig {
// Manual DLT configuration (alternative to @RetryableTopic)
@Bean
public DefaultErrorHandler errorHandler(KafkaTemplate<Object, Object> template) {
// By default, publishes to <original-topic>.DLT
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template);
return new DefaultErrorHandler(recoverer, new FixedBackOff(1000L, 3L));
}
}
// @DltHandler — used with @RetryableTopic for in-class DLT handling
import org.springframework.kafka.annotation.DltHandler;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.KafkaHeaders;
import org.springframework.messaging.handler.annotation.Header;
@DltHandler
public void processDlt(
String message,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
System.err.printf("Failed message from topic %s: %s%n", topic, message);
// persist to database, send alert, etc.
}
Wiring the Error Handler
Register the DefaultErrorHandlerConcurrentKafkaListenerContainerFactory
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.util.backoff.ExponentialBackOff;
@Configuration
public class KafkaConsumerConfig {
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory(
ConsumerFactory<String, String> consumerFactory,
KafkaTemplate<Object, Object> kafkaTemplate) {
ExponentialBackOff backOff = new ExponentialBackOff(1000L, 2.0);
backOff.setMaxAttempts(4);
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(kafkaTemplate);
DefaultErrorHandler errorHandler = new DefaultErrorHandler(recoverer, backOff);
// Skip retry entirely for these — send straight to DLT
errorHandler.addNotRetryableExceptions(
IllegalArgumentException.class,
NullPointerException.class
);
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
factory.setCommonErrorHandler(errorHandler);
return factory;
}
}
addNotRetryableExceptions()DeserializationException.class