What are the Spring Kafka AckModes (RECORD, BATCH, MANUAL, etc.) and how do you use manual acknowledgment?
answer
- AckMode = when offsets get committed
- RECORD per record; BATCH per poll (default)
- TIME/COUNT/COUNT_TIME thresholds
- MANUAL queued vs MANUAL_IMMEDIATE synchronous
- forget acknowledge() => redelivery
basics
~10 sAckMode controls when the container commits offsets. RECORD commits after each record; BATCH commits after the whole poll batch (default); MANUAL/MANUAL_IMMEDIATE hand you an Acknowledgment object so your code decides when to commit.
solid answer
~50 sAckMode is set on ContainerProperties and governs offset-commit timing for successfully processed records. Automatic modes: RECORD commits after every record's listener returns; BATCH (the default) commits all offsets from the last poll() after the batch is processed; TIME commits on an interval (ackTime); COUNT after N records (ackCount); COUNT_TIME whichever first. Manual modes: MANUAL and MANUAL_IMMEDIATE require you to call Acknowledgment.acknowledge() — the container injects an Acknowledgment parameter and disables auto-commit. With MANUAL the ack is queued and committed at the next poll boundary alongside the container's cadence; MANUAL_IMMEDIATE commits synchronously right away. You must also set the consumer's enable.auto.commit=false (Spring does this for you). Manual ack is for precise at-least-once control — commit only after downstream side-effects succeed. Not calling acknowledge() means the offset isn't committed and the record is redelivered after a restart/rebalance.
code
java · 18 lines@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> manualAckFactory(
ConsumerFactory<String, String> cf) {
var factory = new ConcurrentKafkaListenerContainerFactory<String, String>();
factory.setConsumerFactory(cf);
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
@KafkaListener(topics = "payments", containerFactory = "manualAckFactory")
public void handle(String payload, Acknowledgment ack) {
try {
paymentService.apply(payload); // side effect first
ack.acknowledge(); // commit only after success
} catch (TransientException e) {
ack.nack(Duration.ofSeconds(5)); // redeliver after a pause, no commit
}
}go deeper
Know RECORD vs BATCH and that MANUAL means you call acknowledge().
Explain default BATCH, the TIME/COUNT variants, and that Spring disables auto-commit.
Tie manual ack to side-effect-safe at-least-once design, nack, and MANUAL vs MANUAL_IMMEDIATE timing.
Reason about delivery guarantees, idempotency/transactions for effective exactly-once, and commit-cadence trade-offs vs throughput and redelivery windows.
**Why AckMode exists** Kafka tracks how far a consumer group has processed each partition via **committed offsets**. If a consumer dies, a new owner resumes from the last committed offset. **AckMode** (an enum on `ContainerProperties`) decides *when* Spring's container commits those offsets. Spring **disables Kafka's `enable.auto.commit`** and manages commits itself so it can align them with successful listener execution — giving **at-least-once** semantics. **Automatic (container-managed) modes** - **RECORD**: commit the offset after **each** record's listener method returns successfully. Lowest redelivery window (at most one record reprocessed on crash) but most commit overhead. - **BATCH** *(default)*: after the entire batch returned by one `poll()` is processed, commit the offsets for all of them. Good throughput; on crash mid-batch the whole batch is redelivered. - **TIME**: commit when `ackTime` (ContainerProperties.setAckTime) has elapsed since the last commit. - **COUNT**: commit after `ackCount` records processed. - **COUNT_TIME**: commit when **either** the count or the time threshold is hit first. In all automatic modes, a commit only happens for records the listener processed **without throwing** — an unhandled exception routes to the error handler and the offset is not advanced past the failed record (it may be retried/redelivered per the error handler / DefaultErrorHandler config). **Manual modes** - **MANUAL**: the container injects an **Acknowledgment** into your listener; you call `acknowledge()`. The ack is **queued** and the actual commit happens cooperatively at the container's next commit opportunity (out-of-order acks within a batch are handled). Requires the listener signature to include `Acknowledgment`. - **MANUAL_IMMEDIATE**: `acknowledge()` triggers a commit **synchronously and immediately** on the consumer thread. Use manual acks when the commit must be tied to a **side effect** — e.g., only commit after the record has been persisted to a database or forwarded downstream, so a failure before that guarantees redelivery. **How to configure** Set it on the factory's container properties: `factory.getContainerProperties().setAckMode(AckMode.MANUAL_IMMEDIATE)`. Manual modes are only valid when the listener actually accepts an `Acknowledgment` parameter; otherwise the container fails fast at startup. **Edge cases & gotchas** - **Forgetting to call acknowledge()** in a manual mode → offsets never advance; on restart/rebalance those records are **redelivered** (silent duplicate processing / apparent stuck consumer). - Manual ack does **not** make processing exactly-once; it's still at-least-once — design idempotent handlers or use transactions. - **Acknowledgment.nack(Duration)** lets you negatively acknowledge and request redelivery after a pause, without throwing. - With **batch listeners**, a single `Acknowledgment` covers the whole `List<ConsumerRecord>`; you can't ack individual records unless you use the nack-with-index form. - AckMode governs **commit timing**, not delivery — even RECORD is at-least-once because a crash between processing and commit reprocesses the record. - Don't set Kafka's `enable.auto.commit=true` and expect AckMode to work — Spring relies on it being false; the container sets it.
- What is the difference between MANUAL and MANUAL_IMMEDIATE?MANUAL queues your acknowledge() call and lets the container commit at its next opportunity (still respecting ordering); MANUAL_IMMEDIATE performs the offset commit synchronously on the consumer thread the moment you call acknowledge().
- Does using MANUAL ack give you exactly-once processing?No. It's still at-least-once — a crash after the side effect but before the commit reprocesses the record. For exactly-once you need idempotent handlers or Kafka transactions (read-process-write with a KafkaTransactionManager).
- What happens if you never call acknowledge() in a MANUAL listener?The offset is never committed, so after a restart or rebalance the consumer resumes from the old offset and reprocesses those records — looks like duplicates or a stuck/looping consumer.
saying these in an interview costs you the question
- Saying manual ack gives exactly-once semantics
- Thinking BATCH is per-record (it's per poll batch)
- Believing enable.auto.commit should be true with AckMode
- Assuming a thrown exception commits the offset anyway