Medium java spring

When do you retry immediately, use exponential backoff, or send a message to a dead-letter topic

The 30 second answer

I retry immediately only for a clearly transient, very short failure, and even then I cap the attempts. For anything that may stay down, I use exponential backoff with jitter, and I keep the work inside max.poll.interval.ms so the member is not evicted. After the limit, or for a deterministic error such as a bad schema, I send the record to a dead-letter topic. Indefinite retries on a poison message just block the partition.

The choice is about the shape of the failure, not about habit.

Retry immediately, and only a few times, when the error is clearly transient and short: a momentary connection reset, a single lock timeout. I still cap the attempts. An unbounded tight loop turns a brief blip into a self-inflicted outage, and it holds the partition for everyone else.

Exponential backoff, preferably with jitter, when the dependency may stay down for a while: a database failover, a 429 or 503, a network partition. Immediate retries at full rate would hammer a service that is already in trouble. Spring Kafka's DefaultErrorHandler uses a BackOff; ExponentialBackOffWithMaxRetries exists because a plain ExponentialBackOff from Spring Framework retries indefinitely unless you compute a max elapsed time. Blocking backoff happens on the listener thread, so the total pause must stay comfortably inside max.poll.interval.ms or the member is evicted and the rebalance makes the outage worse.

Dead-letter (or a retry topic, then a dead-letter) when I can predict that more attempts will not help, or when I have already exhausted the capped backoff. Validation errors, unknown message types, and persistent business rule failures belong here on the first or second failure. A poison record that is retried forever is an operational incident, not resilience.

Non-blocking retries. Since Spring Kafka 2.7, @RetryableTopic and RetryTopicConfiguration forward a failed record to a retry topic with a backoff timestamp, and eventually to a dead-letter topic. The main partition can move on. The documented pattern is: failed record goes to a retry topic, the retry consumer waits until the timestamp is due, and after the attempts are exhausted the record can go to a DLT. The cost is ordering: a later record on the original partition may be processed before the retried one. I use this when availability of the rest of the stream matters more than strict per-key order. It also cannot be combined with container transactions, and it is not supported for batch listeners, according to the same reference.

Editor's note: "Retry immediately or backoff or DLT" is not a single global setting. I classify by exception type, I cap every path, and I watch both the main lag and the dead-letter rate.

Sources (5)
  1. Spring for Apache Kafka: Handling exceptions (`DefaultErrorHandler`, backoff)docs.spring.io
  2. Spring for Apache Kafka: Non-blocking retries (`@RetryableTopic`)docs.spring.io
  3. Spring for Apache Kafka: How the retry-topic pattern worksdocs.spring.io
  4. Spring for Apache Kafka: Retry topic features (backoff)docs.spring.io
  5. Kafka 4.4 consumer configs (`max.poll.interval.ms`)kafka.apache.org