At-Least-Once Delivery
IntermediateAt-least-once guarantees no message is lost; consumers may process duplicates after rebalance or restart, so consumer logic must be idempotent.
Overview
At-least-once delivery is the default and most practical delivery guarantee in Kafka. It means every message will eventually be processed — but may be processed more than once in failure scenarios. This happens because Kafka commits offsets separately from the processing of records: if a consumer processes a batch and then crashes before committing offsets, the next consumer (or the same one after restart) will re-read and re-process those records from the last committed offset. At-least-once is the pragmatic choice for most applications because it never loses data, and the duplicate risk can be managed by making consumer logic idempotent. It is the foundation from which exactly-once semantics are built.
How Duplicates Arise
Duplicates happen when a consumer processes records successfully but fails to commit the offset before crashing:
1. Consumer polls records [100..110] from partition 3. 2. Consumer processes records 100–110 successfully (DB writes done). 3. Consumer crashes before calling commitSync() / commitAsync(). 4. Rebalance: another consumer (or the same on restart) takes partition 3. 5. It resumes from the last committed offset — say, 100. 6. Records 100–110 are re-processed → **duplicates**.
The same scenario applies to rebalances: if the consumer is mid-batch when a rebalance starts and it does not commit the partial batch first, the new assignee replays those records.
// Safe at-least-once consumer — commit AFTER processing
Properties props = new Properties();
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // critical
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(List.of("orders"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, String> r : records) {
processOrder(r.value()); // DB write, side effects
}
// Only commit after ALL records in this batch are processed
// If we crash before this line, these records are re-processed on restart
consumer.commitSync();
}
// Scenario: crash after processOrder() but before commitSync()
// → Kafka re-delivers the same records on restart
// → processOrder() runs again → DUPLICATEIdempotent Consumers — Making Duplicates Safe
The standard fix for at-least-once delivery is idempotent processing: design consumer logic so that re-processing the same message produces the same result as processing it once. Two common patterns:
**Database deduplication**: Store the Kafka record's topic+partition+offset (or a business event ID) in a processed_events table. Check before processing; skip if already seen. Wrap in a transaction.
**Natural idempotency**: Some operations are naturally idempotent — an INSERT ... ON DUPLICATE KEY UPDATE, a PUT request to an external API that uses the record's ID as the resource ID, or setting a field to a specific value.
@KafkaListener(topics = "orders", groupId = "order-processor")
@Transactional
public void onOrder(ConsumerRecord<String, OrderEvent> record) {
// Build a unique key from Kafka coordinates
String eventKey = record.topic() + "-" + record.partition() + "-" + record.offset();
// Deduplication check — same DB transaction as the business operation
if (processedEventRepo.existsByKey(eventKey)) {
log.info("Skipping duplicate: {}", eventKey);
return;
}
// Business logic
orderService.createOrder(record.value());
// Mark as processed — atomic with the business write
processedEventRepo.save(new ProcessedEvent(eventKey, Instant.now()));
// Both writes are in the same @Transactional boundary
// If anything fails, both roll back — the record will be reprocessed safely
}At-Least-Once on the Producer Side
Duplicates can also arise from the producer side. With acks=all and retries > 0, a producer may retry a send after a transient network error even though the broker already persisted the record (but the ack was lost in transit). The result: two identical records in the partition. Enable idempotent producer (enable.idempotence=true) to deduplicate retries at the broker level using sequence numbers per partition.
// Producer-side deduplication: enable idempotent producer
Properties props = new Properties();
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
// Automatically sets: acks=all, retries=MAX_VALUE, max.in.flight.requests=5
// Without idempotence:
// 1. Producer sends record [PID=-, seq=-] → broker writes it → ACK lost
// 2. Producer retries → broker writes it again → DUPLICATE
// With idempotence:
// 1. Producer sends [PID=42, seq=0] → broker writes, stores seq
// 2. Producer retries [PID=42, seq=0] → broker sees same seq → DISCARDS
// Application-level correlation ID as an alternative
@Service
public class OrderEventPublisher {
public void publish(Order order) {
OrderEvent event = OrderEvent.builder()
.eventId(order.getId().toString()) // stable, deterministic ID
.orderId(order.getId())
.build();
// Consumer can use eventId to deduplicate
kafka.send("orders", order.getId().toString(), event);
}
}Key Points to Remember
- 1At-least-once: every record is processed at least once — duplicates can occur when a crash happens between processing and committing the offset.
- 2Commit offsets AFTER processing (manual commit, enable.auto.commit=false) to guarantee at-least-once; committing before risks at-most-once (data loss).
- 3Make consumer logic idempotent: use a processed_events deduplication table keyed by topic+partition+offset.
- 4Producer-side duplicates: a producer retry after a lost ACK can also produce duplicates; enable idempotence=true to prevent this.
- 5At-least-once is the pragmatic default for most systems; exactly-once is only necessary when duplicate side effects are unacceptable (e.g., charge a payment card).
- 6Natural idempotency (INSERT ON DUPLICATE KEY, upserts) is preferable to deduplication tables where possible — simpler and faster.
Interview Questions
Sign in to ask AriaWhat is at-least-once delivery in Kafka and when do duplicates occur?
How do you make a Kafka consumer idempotent?
Can duplicates arise from the producer side in Kafka? How do you prevent them?
What is the difference between at-least-once and at-most-once delivery?
You have a financial system processing payment events from Kafka. How do you prevent double-charging a customer?
Ask Aria about At-Least-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.