Kafka
Reactive Programming
Stream Processing
System Failure
Consumer Stream Restart

Gracefully restart a Reactive-Kafka Consumer Stream on failure

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

When a reactive Kafka consumer stream fails, the right response is not an immediate blind restart loop. A graceful restart strategy should preserve offset correctness, avoid thrashing the broker, and make the failure visible enough that operators can tell whether the stream recovered or is stuck in a crash cycle.

In Reactor Kafka or similar reactive systems, that usually means two separate design decisions: handle record-level failures inside the stream when possible, and apply retry with backoff around the stream boundary when the subscription itself dies.

Distinguish Record Failures From Stream Failures

Not every error deserves a full consumer restart. A malformed record or a business-validation failure is often a record-level issue, not a reason to tear down the entire subscription.

The stream itself usually starts at KafkaReceiver.receive():

java
1import java.util.Collections;
2import reactor.kafka.receiver.KafkaReceiver;
3import reactor.kafka.receiver.ReceiverOptions;
4
5ReceiverOptions<String, String> options =
6    ReceiverOptions.<String, String>create(props)
7        .subscription(Collections.singleton("orders"));
8
9KafkaReceiver<String, String> receiver = KafkaReceiver.create(options);

That receive Flux is the boundary where full-stream restart logic belongs. Inside that boundary, you should try to handle bad individual records locally.

Restart the Stream With Backoff

If the subscription fails because of broker availability, infrastructure trouble, or some fatal upstream condition, wrap the stream with backoff-based retry:

java
1import java.time.Duration;
2import reactor.util.retry.Retry;
3
4receiver.receive()
5    .doOnNext(record -> {
6        System.out.println("Received: " + record.value());
7        record.receiverOffset().acknowledge();
8    })
9    .retryWhen(
10        Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1))
11            .maxBackoff(Duration.ofSeconds(30))
12            .doBeforeRetry(signal ->
13                System.err.println("Restarting after: " + signal.failure().getMessage()))
14    )
15    .subscribe();

The backoff is the important part. Without it, a persistent failure can create a tight restart loop that wastes resources and makes the system harder to recover.

Handle Poison Records Inside the Pipeline

If one record fails because of parsing or business logic, it is often better to handle that record and continue than to let the whole stream collapse:

java
1receiver.receive()
2    .flatMap(record ->
3        process(record.value())
4            .doOnSuccess(v -> record.receiverOffset().acknowledge())
5            .onErrorResume(ex -> {
6                System.err.println("Bad record: " + ex.getMessage());
7                return sendToDeadLetter(record, ex)
8                    .doOnSuccess(v -> record.receiverOffset().acknowledge())
9                    .then();
10            })
11    )
12    .retryWhen(Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1)))
13    .subscribe();

This separates poison-message handling from infrastructure recovery. That distinction is essential. Otherwise, one bad record can trigger endless full-stream restarts.

Acknowledge Offsets Only After Safe Processing

A restart strategy is graceful only if offset handling is correct. The normal rule is:

  • acknowledge after successful processing
  • or acknowledge after safe dead-letter handling
  • do not acknowledge before the outcome is durable

If you acknowledge too early, a later crash can lose messages. If you acknowledge too late or not at all, you can create repeated reprocessing loops that look like restart problems even though the real issue is offset timing.

Make Restart Visibility Part of the Design

Silent restarts are operationally weak. Log the failure cause, count restart attempts, and alert when restart frequency crosses a useful threshold.

One restart after a temporary broker hiccup is normal. A stream that restarts every few seconds for an hour is not healthy, even if the process stays technically alive. Observability needs to make that obvious.

Avoid Restarting the Whole Application

It is tempting to respond to a failed consumer by restarting the entire service. That is usually too coarse. In reactive systems, the consumer subscription is often the right failure boundary. Restarting the stream is faster, less disruptive, and easier to reason about.

Only restart the whole application when the entire process state is compromised, not just the Kafka subscription.

Common Pitfalls

The most common mistake is retrying immediately with no backoff. That turns transient outages into noisy loops and can make a bad situation worse.

Another common issue is letting per-record business exceptions escape and kill the entire consumer stream. Many failures should be handled at the record level instead.

Developers also acknowledge offsets too early. That makes restart behavior appear clean while actually dropping work after a crash.

Finally, do not treat automatic restart as a substitute for observability. If a stream keeps restarting, operators need to know why and how often.

Summary

  • Graceful restart means controlled resubscription with backoff, not a tight retry loop.
  • Apply restart logic at the stream boundary around receive().
  • Handle poison records inside the stream when possible so one bad message does not kill the whole consumer.
  • Acknowledge offsets only after processing or dead-letter handling is safely complete.
  • Log and monitor restart causes so recurring failures are visible instead of silently masked.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.