Kafka Acknowledgment Modes — Manual vs Auto
Fast recall
Quick revision
Core answer
Understanding of at-least-once delivery and offset commit timing
Using auto-commit without realizing messages could be lost if the consumer crashes after commit but before processing
Explain the at-least-once vs at-most-once trade-off and how idempotent consumers handle redelivery
Interview-ready guide
Interview answer
Here's the tension most developers miss with Kafka acknowledgment. Commit before processing — message lost if processing fails. Commit after processing — message duplicated if consumer crashes between processing and commit. No mode gives exactly-once alone.
Spring Kafka defaults to AckMode.BATCH — commits after all records from a single poll() are processed. If the listener throws, earlier records are re-delivered. This is at-least-once delivery.
AckMode.MANUAL gives full control — inject Acknowledgment and call acknowledge() after your database transaction succeeds. If you forget to call it, messages re-process indefinitely.
For exactly-once, use Kafka transactions — consumed offset and produced messages commit atomically. Set isolation.level=read_committed.
Regardless of mode, make listeners idempotent. Store a deduplication key and check before processing. This is the pragmatic defense.
In practice, I use MANUAL with idempotent listeners for critical data and BATCH for analytics/logging where occasional duplicates are acceptable.
Kafka Acknowledgment Modes — Manual vs Auto
Direct Answer
Spring Kafka's AckMode determines when consumer offsets are committed. BATCH (default) commits after each poll batch. MANUAL requires calling Acknowledgment.acknowledge() explicitly, giving full control over exactly when a message is considered processed.
Code Example
Auto-commit lets the consumer periodically commit offsets in the background — convenient but loses messages when processing fails after the commit fires. Manual commit waits for your business logic to succeed before acknowledging — at-least-once delivery, the safer default.
# AUTO COMMIT (default) — DO NOT use when message loss is unacceptable
spring:
kafka:
consumer:
enable-auto-commit: true
auto-commit-interval-ms: 5000 # commit every 5s in the background
# MANUAL — recommended for any business-critical pipeline
spring:
kafka:
consumer:
enable-auto-commit: false
listener:
ack-mode: manual_immediate # commit when you call ack.acknowledge()
@Configuration
class ManualAckConfig {
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> factory(
ConsumerFactory<String, String> cf) {
ConcurrentKafkaListenerContainerFactory<String, String> f = new ConcurrentKafkaListenerContainerFactory<>();
f.setConsumerFactory(cf);
f.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return f;
}
}
@Component
public class PaymentListener {
@KafkaListener(topics = "payments")
public void onPayment(PaymentEvent ev, Acknowledgment ack) {
try {
paymentService.process(ev); // business logic
ack.acknowledge(); // commit ONLY after success
} catch (TransientException e) {
// do NOT ack — Kafka will redeliver after consumer poll resumes
log.warn("Transient failure for {}, will retry", ev.id(), e);
throw e; // triggers configured ErrorHandler
} catch (PoisonMessageException e) {
log.error("Poison message {}, sending to DLT", ev.id(), e);
ack.acknowledge(); // ack so we don't reprocess forever
dltTemplate.send("payments.DLT", ev);
}
}
}
| Ack mode | Commit timing | Risk |
|---|---|---|
RECORD | after each record returns | lowest loss, slowest |
BATCH (default for batch listeners) | after the entire poll batch | small batch-loss window |
TIME | every N ms | up to N ms of replay on crash |
COUNT | every N messages | up to N messages replay on crash |
MANUAL | when ack.acknowledge() is called, deferred | gives you control |
MANUAL_IMMEDIATE | commit immediately on ack.acknowledge() | strongest at-least-once |
- Auto-commit is fast and easy — but a crash after the auto-commit fires and before processing completes loses the message silently. Avoid for anything that touches money, inventory, or external systems.
- Manual immediate is the production default for at-least-once processing — your handler must therefore be idempotent (use a dedup table keyed by
topic:partition:offsetor business key). - Never ack inside a
tryblock before processing succeeds — that's "silent loss masquerading as manual commit". - For exactly-once, combine a transactional producer (
isolation.level=read_committedon consumer) with the database write in the same Kafka transaction (KafkaTransactionManager).
Common Mistakes & Pitfalls
Using auto-commit without realizing messages could be lost if the consumer crashes after commit but before processing
Check your understanding
Answer these without looking back at the article.
- How does idempotent consumer processing handle duplicate message delivery?
- What is the difference between at-least-once and exactly-once delivery?
- How do you implement exactly-once semantics with Kafka transactions?
Related interview prep categories
2Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.