At-Most-Once Delivery
IntermediateAt-most-once commits offsets before processing; a crash after commit but before processing causes data loss — acceptable for metrics but not for financial or order data.
Overview
At-most-once delivery guarantees that a message will be processed zero or one time — never more than once. The trade-off is that messages can be lost: if a consumer commits its offset before successfully processing the record, and then crashes, the record is never reprocessed. Data loss is accepted in exchange for no duplicates. This is the semantics of enable.auto.commit=true in Kafka — offsets are periodically committed regardless of whether the records in that batch have been fully processed. At-most-once is appropriate for non-critical, high-throughput telemetry where occasional data loss is acceptable and duplicate events would cause more harm than missing ones.
How At-Most-Once Causes Data Loss
At-most-once delivers via commit-before-process:
1. Consumer polls records [100..110]. 2. Consumer immediately commits offset 110 (before processing). 3. Consumer begins processing record 100. 4. Consumer crashes at record 105. 5. On restart, consumer resumes from offset 111 — records 105–110 are **permanently lost**.
With enable.auto.commit=true, the auto-commit timer may fire between poll() and the end of your processing loop, creating the same window.
// At-most-once — commit BEFORE processing (data loss if crash)
Properties props = new Properties();
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(List.of("metrics"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
// COMMIT FIRST — at-most-once: crash after this but before processing = data loss
consumer.commitSync();
for (ConsumerRecord<String, String> record : records) {
// If we crash here, records are already committed — will NOT be reprocessed
processMetric(record.value());
}
}
// enable.auto.commit=true produces the same at-most-once window:
// auto-commit fires after poll() but before processing completesWhen At-Most-Once Is Acceptable
At-most-once is the right choice when:
1. **Missing a record is harmless** — metrics, telemetry, analytics aggregations where one missed data point has negligible impact. 2. **Duplicates are MORE harmful than loss** — e.g., real-time dashboards where a duplicate counter increment is visible, but a missing counter is not. Or push notifications where double-sending is worse than missing one. 3. **Maximum throughput is required** — removing the write-then-process round-trip reduces latency.
Never use at-most-once for: financial transactions, order processing, inventory updates, user-facing state mutations.
// at-most-once use case: metrics/telemetry (loss acceptable)
@KafkaListener(topics = "app-metrics", groupId = "metrics-aggregator")
public void onMetric(AppMetric metric) {
// If this metric is lost due to consumer restart, it is acceptable
// Missing one data point in a 1-minute aggregate is negligible
metricsAggregator.record(metric.getName(), metric.getValue());
}
# application.yml — auto-commit for non-critical consumers
spring:
kafka:
consumer:
enable-auto-commit: true
auto-commit-interval: 1000 # commit every 1 second
// at-most-once NEVER appropriate for:
@KafkaListener(topics = "payments")
public void processPayment(Payment payment) {
// A lost payment = unhappy customer + revenue loss
// MUST use at-least-once with idempotent processing here
chargeCard(payment);
}Delivery Semantics Comparison
Understanding all three delivery semantics side-by-side is essential for system design decisions.
// Delivery Semantics Comparison:
//
// ┌─────────────────┬──────────────────┬────────────────────────────────────────┐
// │ Semantic │ Commit Timing │ Risk │
// ├─────────────────┼──────────────────┼────────────────────────────────────────┤
// │ At-most-once │ Before process │ Data loss on crash — no duplicates │
// │ At-least-once │ After process │ Duplicates on crash — no data loss │
// │ Exactly-once │ Atomic with work │ Neither — requires transactions/EOS │
// └─────────────────┴──────────────────┴────────────────────────────────────────┘
//
// In practice:
// At-most-once → enable.auto.commit=true, or commitSync() before processing
// At-least-once → enable.auto.commit=false, commitSync/Async AFTER processing
// Exactly-once → Kafka transactions (transactional.id + read_committed) or
// Kafka Streams (processing.guarantee=exactly_once_v2)
//
// Default behaviour of Spring @KafkaListener: at-least-once
// (auto-commit is disabled by Spring Kafka by default)Key Points to Remember
- 1At-most-once: commit offsets BEFORE processing — crash after commit but before process = data loss, but no duplicates.
- 2enable.auto.commit=true with a periodic timer creates the at-most-once window automatically.
- 3Acceptable for: non-critical metrics, telemetry, analytics where occasional loss is negligible.
- 4Never use for: financial data, order state, inventory, any user-facing mutation.
- 5Spring Kafka defaults to at-least-once (disables auto-commit); override only intentionally.
- 6The three semantics compare as: at-most-once (loss OK), at-least-once (duplicate OK), exactly-once (neither, but has overhead).
Interview Questions
Sign in to ask AriaWhat is at-most-once delivery semantics in Kafka and how is it achieved?
How does enable.auto.commit=true create an at-most-once delivery window?
When would you deliberately choose at-most-once over at-least-once delivery?
Compare at-most-once, at-least-once, and exactly-once delivery semantics with their trade-offs.
What is the default delivery semantic of Spring Kafka @KafkaListener and why?
Ask Aria about At-Most-Once Delivery
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.