skip to content

What are the Spring Kafka AckModes (RECORD, BATCH, MANUAL, etc.) and how do you use manual acknowledgment?

level: seniorimportance: must knowfreq 74%

answer

  1. AckMode = when offsets get committed
  2. RECORD per record; BATCH per poll (default)
  3. TIME/COUNT/COUNT_TIME thresholds
  4. MANUAL queued vs MANUAL_IMMEDIATE synchronous
  5. forget acknowledge() => redelivery

basics

~10 s

AckMode 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 s

AckMode 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
java
@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

for a junior

Know RECORD vs BATCH and that MANUAL means you call acknowledge().

for a middle

Explain default BATCH, the TIME/COUNT variants, and that Spring disables auto-commit.

for a senior

Tie manual ack to side-effect-safe at-least-once design, nack, and MANUAL vs MANUAL_IMMEDIATE timing.

for a principal

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

context