Kafka Producer: Getting acks, retry, and idempotence Right
1. Overview
When a producer sends a message to Kafka, there are three questions you must answer: when is sending considered successful, how should you retry on error, and does retry cause message duplication. This article explains acks, retries, and enable.idempotence — three configurations that determine the reliability and correctness of a producer. After reading, you’ll know how to configure a “production-ready” producer and avoid common mistakes that cause data loss or duplication.
2. Required Dependencies
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.7.0</version>
</dependency>
The examples in this article use the pure Kafka client (KafkaProducer), not Spring Kafka, so you can clearly see how each configuration affects behavior.
3. acks: When a Producer Considers Sending Successful
acks determines how many broker acknowledgments the producer must wait for before considering a message sent.
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.ACKS_CONFIG, "all");
acks=0: the producer doesn’t wait for any acknowledgments. Fastest, but messages are lost if a broker goes down immediately after receiving — you won’t even know they’re lost.acks=1: only waits for the leader partition to write to its local log and return an ACK. If the leader crashes before followers can sync, the message is lost.acks=all(or-1): waits for all replicas in the ISR (in-sync replica set) to acknowledge. Safest, but higher latency.
Note:
acks=allis truly safe only when paired withmin.insync.replicas >= 2on the broker. Ifmin.insync.replicas=1,acks=allonly requires 1 replica to acknowledge — no different fromacks=1in terms of safety.
3.1. Accompanying Broker Configuration
# server.properties or topic config
min.insync.replicas=2
With replication factor = 3 and min.insync.replicas=2, the system can tolerate 1 broker going down while still writing, and ensures data is on at least 2 copies before acknowledging.
4. retries: Handling Temporary Errors
A producer may encounter temporary errors like NotLeaderForPartitionException or brief network disconnections. retries allows the producer to automatically resend without manual error handling code.
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 100);
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120_000);
retries: maximum number of retry attempts. From Kafka 2.1 onward, set this very high (orInteger.MAX_VALUE) and usedelivery.timeout.msto control total wait time instead of counting attempts.retry.backoff.ms: wait time between retry attempts, preventing broker spam.delivery.timeout.ms: maximum total time from callingsend()until getting a result (success or final error). Must be larger thanrequest.timeout.ms + linger.ms.
Problem: without care, retries can cause message duplication or out-of-order delivery.
4.1. Why Retries Cause Message Duplication
1. Producer sends batch containing message A
2. Broker writes A successfully, sends ACK back
3. ACK is lost on the network (network timeout)
4. Producer doesn't receive ACK -> considers it a failure -> retries
5. Broker receives request again -> writes A a second time
=> Topic now has 2 copies of message A
This is a classic scenario: the broker successfully wrote the message but the producer doesn’t know it, so it resends. The idempotence section later addresses this problem correctly.
4.2. Why Retries Cause Out-of-Order Delivery
If max.in.flight.requests.per.connection > 1, the producer can send multiple batches in parallel. Batch 2 may be written by the broker before batch 1 if batch 1 errors and is being retried.
max.in.flight.requests.per.connection=5
With this configuration but without idempotence, message order in the partition is not guaranteed when retries occur.
5. Idempotence: Solving Duplication from Retries
enable.idempotence=true helps the producer automatically avoid duplicate writes when retrying, thanks to Kafka attaching a Producer ID (PID) and sequence number to each batch.
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
When idempotence is enabled, Kafka automatically sets the following values (you don’t need to configure them yourself, but should be aware):
acks = all
retries = Integer.MAX_VALUE (or very high)
max.in.flight.requests.per.connection <= 5
5.1. How It Works
Each producer session is assigned a unique PID by the broker. Each message sent within a partition has an incrementing sequence number. The broker stores the last sequence number written for each PID + partition combination.
Producer sends batch seq=5 for partition 0
Broker: "last sequence I stored is 4, 5 is next -> write it, store seq=5"
Producer resends (due to retry) batch seq=5
Broker: "seq=5 I already wrote -> skip it, return success ACK anyway"
As a result, retries don’t create duplicate records — the broker automatically detects and discards duplicates based on the sequence number, without requiring manual handling by the producer or consumer.
Note: idempotence only guarantees no duplicates within a single producer session and within a single partition. If the producer restarts (a new PID is assigned), deduplication by the old PID no longer applies. To achieve exactly-once across multiple partitions/topics, you need transactions (
transactional.id) — beyond the scope of simple idempotence.
5.2. Example: Production-Ready Producer Configuration
public ProducerFactory() {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// Enable idempotence -> automatically sets acks=all, retries high, in-flight <= 5
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120_000);
this.producer = new KafkaProducer<>(props);
}
public void sendOrderEvent(String orderId, String payload) {
ProducerRecord<String, String> record =
new ProducerRecord<>("order-events", orderId, payload);
producer.send(record, (metadata, exception) -> {
if (exception != null) {
log.error("Failed to send order {}", orderId, exception);
} else {
log.info("Order {} written to partition {} offset {}",
orderId, metadata.partition(), metadata.offset());
}
});
}
ENABLE_IDEMPOTENCE_CONFIG = trueis the most important line — it automatically pulls inacks=all.- The callback in
send()shows the actual result: ifexception != null, it means retries exhausteddelivery.timeout.msand still failed — this is when you need to handle the business logic (log, alert, save to error table).
6. Verifying Behavior with Tests
To illustrate deduplication, let’s create a simple MessageDeduplicator that mimics the sequence number mechanism at the application level (useful when you can’t enable idempotence, such as in legacy systems):
public class MessageDeduplicator {
private final Map<String, Long> lastSeenSequence = new ConcurrentHashMap<>();
public boolean shouldWrite(String partitionKey, long sequence) {
Long last = lastSeenSequence.get(partitionKey);
if (last != null && sequence <= last) {
return false; // duplicate, skip
}
lastSeenSequence.put(partitionKey, sequence);
return true;
}
}
class MessageDeduplicatorTest {
@Test
void shouldSkipDuplicateSequence() {
MessageDeduplicator deduplicator = new MessageDeduplicator();
assertThat(deduplicator.shouldWrite("order-1", 5)).isTrue();
assertThat(deduplicator.shouldWrite("order-1", 5)).isFalse(); // retry same seq
assertThat(deduplicator.shouldWrite("order-1", 6)).isTrue();
}
@Test
void shouldAllowDifferentPartitionKeysIndependently() {
MessageDeduplicator deduplicator = new MessageDeduplicator();
assertThat(deduplicator.shouldWrite("order-1", 1)).isTrue();
assertThat(deduplicator.shouldWrite("order-2", 1)).isTrue();
}
}
The first test asserts that the same sequence sent again is skipped — true to the spirit of producer idempotence. The second test asserts that deduplication applies only within a key/partition scope and doesn’t affect other keys.
7. Configuration Summary by Use Case
| Use Case | acks | enable.idempotence | Risk |
|---|---|---|---|
| Logs, non-critical metrics | 0 | false | Message loss on error, but highest throughput |
| Balance between speed and safety | 1 | false | Message loss if leader crashes before followers sync |
| Critical business data (orders, payments) | all | true | Higher latency, but no loss or duplication from retries |
8. Common Mistakes
- Setting
acks=allbut forgettingmin.insync.replicas: you think it’s safe but the broker only needs 1 ISR to ACK — no different fromacks=1. - Writing your own retry logic alongside Kafka client’s default retries: easily causes duplicate sends because both retry layers are active.
- Enabling idempotence but setting
max.in.flight.requests.per.connection > 5: Kafka will reject this configuration immediately during producer initialization (ConfigException), as it exceeds the limit supported by idempotence. - Confusing idempotence with system-wide exactly-once: idempotence only protects producer-to-broker. If a consumer processes a message then makes a non-idempotent side-effect call (sending email, deducting funds), you can still have duplicates on the consumer side.
Note: don’t enable
enable.idempotence=trueif your system is running Kafka versions older than 0.11 — this feature is not yet supported by the broker and the producer will fail on initialization.
9. Conclusion
acks, retries, and enable.idempotence are three inseparable pieces when you need a reliable producer. acks=all ensures data is replicated sufficiently before considering it successful; retries help overcome temporary errors; and idempotence ensures those retries don’t create duplicate records. For critical business data, use all three together by default — the latency cost is far less than the cost of dealing with incorrect or duplicate data later.