Kafka Consumer Graceful Shutdown: Handle WakeupException and Commit Offsets Safely
In the previous article, we looked at how rebalancing works and why consumers pause during deployments.
Now let’s look at something that causes even more real production issues:
How do you gracefully shut down a Kafka consumer without losing work?
In real systems:
- Pods restart
- Deployments happen
- Servers receive SIGTERM
- Auto-scaling removes instances
If shutdown is not handled properly, you can:
- Reprocess messages
- Lose uncommitted offsets
- Cause inconsistent state
- Trigger unnecessary rebalances
Kafka does not handle this automatically for you.
You have to shut down the consumer correctly.
Why Graceful Shutdown Matters
A typical Kafka consumer runs inside a loop:
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
process(record);
}
}
Now imagine the application is terminated while:
- Records are already processed
- Offsets are not committed yet
When the consumer restarts, Kafka will deliver those records again.
That may be acceptable.
But in many systems, it creates duplicate writes, duplicate payments, or inconsistent updates.
A proper Kafka consumer graceful shutdown should:
- Stop polling new records
- Finish processing current records
- Commit offsets safely
- Close the consumer cleanly
The Main Problem: poll() Blocks
The poll() method blocks while waiting for records.
If your application receives a shutdown signal while the thread is inside poll(), how do you stop it?
You should not kill the thread.
Kafka provides a safe mechanism for this.
WakeupException in Kafka
Kafka provides this method:
consumer.wakeup();
When called from another thread, it interrupts the blocking poll() call.
Inside the consumer thread, poll() throws:
WakeupException
This is the official and safe way to interrupt a Kafka consumer.
Nothing else should be used.
Correct Kafka Consumer Graceful Shutdown Pattern
Let’s build this properly.
Step 1: Add a running flag
private volatile boolean running = true;
Step 2: Consumer implementation
public class GracefulKafkaConsumer implements Runnable {
private final KafkaConsumer<String, String> consumer;
private volatile boolean running = true;
public GracefulKafkaConsumer(KafkaConsumer<String, String> consumer) {
this.consumer = consumer;
}
@Override
public void run() {
try {
consumer.subscribe(List.of("my-topic"));
while (running) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
process(record);
}
// Commit after processing
consumer.commitSync();
}
} catch (WakeupException e) {
// Expected during shutdown
if (running) {
throw e;
}
} finally {
try {
consumer.commitSync(); // final safety commit
} finally {
consumer.close(); // leave group cleanly
}
}
}
public void shutdown() {
running = false;
consumer.wakeup();
}
private void process(ConsumerRecord<String, String> record) {
System.out.println("Processing: " + record.value());
}
}
What Is Happening Here?
runningcontrols the loop.consumer.wakeup()interruptspoll().WakeupExceptionis expected during shutdown.- Offsets are committed after processing.
consumer.close()triggers proper group leave.
This ensures offsets are committed safely before exit.
Why Not Just Kill the Thread?
If you forcefully stop the thread:
- Offsets may not be committed
- The consumer may not leave the group cleanly
- Rebalances may be delayed
- Duplicate processing may increase
Always use consumer.wakeup() for graceful shutdown.
Manual Commit vs Auto Commit
If you are using:
enable.auto.commit=true
Offsets are committed periodically.
But auto commit does not guarantee that processing is finished before commit.
For production systems, it is usually safer to use:
enable.auto.commit=false
And commit offsets only after successful processing.
If you want a deeper explanation of offset commits, I covered it here:
👉 Kafka Auto Commit Explained (At-Least-Once Processing)
Offset management and graceful shutdown are closely connected.
A Quick Note on Long Processing
If your message processing takes a long time, Kafka may trigger a rebalance even if the consumer is still running.
This usually relates to how frequently poll() is called and certain consumer configurations.
This topic deserves a separate discussion because it directly impacts stability in high-throughput systems.
We’ll explore it properly in a dedicated article.
Connecting Shutdown to JVM Exit
You can connect shutdown logic like this:
GracefulKafkaConsumer consumerRunnable =
new GracefulKafkaConsumer(consumer);
Thread consumerThread = new Thread(consumerRunnable);
consumerThread.start();
Thread mainThread = Thread.currentThread();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
consumerRunnable.shutdown();
try {
mainThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}));
When the application receives a termination signal, the consumer exits safely.
How This Relates to Rebalancing
In the previous article(👉 Read: Kafka Eager vs Cooperative Rebalancing Explained), we discussed eager vs cooperative rebalancing.
Improper shutdown can trigger unnecessary rebalances.
A clean shutdown:
- Reduces duplicate processing
- Leaves the consumer group smoothly
- Minimizes disruption during deployments
Graceful shutdown is not optional in production systems.
It is part of building reliable Kafka consumers.
Closing Thoughts
Kafka consumer graceful shutdown is simple in concept, but easy to get wrong.
The correct approach is:
- Stop polling
- Finish processing
- Commit offsets safely
- Close the consumer properly
If you skip any of these steps, you increase the chance of duplicate processing or inconsistent state.
What’s Next?
Next, we’ll explore Kafka producer internals — specifically how acks, retries, and idempotent producers impact reliability and delivery guarantees in production systems.
If you found this useful and want to share your thoughts, this article is also published on Dev.to where discussions are more active. You can read it there and leave a comment if you’d like:
I always appreciate feedback and different perspectives.