Acks & Durability
The acks producer setting is the single most important knob for write durability in Kafka. It decides how many replica brokers must confirm a record before the producer treats the send as successful, directly trading throughput and latency against the risk of losing data when a broker crashes. Getting it wrong is how teams silently lose messages in production, so it pays to understand exactly what each value waits for and how it combines with min.insync.replicas and the topic’s replication factor.
How acks works
When a producer sends a record, it goes to the leader replica for the target partition. The leader appends the record to its log and, depending on the topic configuration, replicates it to the follower replicas in the in-sync replica (ISR) set. The acks setting controls how long the leader waits before sending an acknowledgement back to the producer.
| acks | Leader waits for | Latency | Data-loss window |
|---|---|---|---|
0 | Nothing — fire and forget | Lowest | High: record lost if the send fails or the leader is down |
1 | Leader’s own log write | Low | Medium: record lost if the leader crashes before followers replicate |
all (-1) | All in-sync replicas to replicate | Highest | Lowest: record survives as long as one ISR member survives |
With acks=0 the producer never knows whether the broker received the record. With acks=1 the leader confirms after writing to its own log but before followers have copied it. With acks=all the leader only confirms once every replica in the current ISR has the record.
Failure scenarios and data-loss windows
The danger with acks=1 is the gap between the leader’s local write and follower replication. Consider replication factor 3:
1. Producer sends record R; leader writes R to its log and returns ack.
2. Producer considers R durable and moves on.
3. Leader crashes BEFORE followers replicate R.
4. A follower that never received R is elected the new leader.
5. R is gone — permanently, with no error ever surfaced to the producer.
acks=all closes this window. The leader will not acknowledge until the followers in the ISR have the record, so a freshly elected leader is guaranteed to already hold it.
Warning:
acks=allalone is not enough. If the ISR shrinks to just the leader (all followers fall behind), an “all” ack only means the leader has the data — you are effectively back toacks=1. This is exactly whatmin.insync.replicasguards against.
Durable writes: acks=all + min.insync.replicas=2 + RF=3
min.insync.replicas (a broker/topic setting, not a producer setting) defines the minimum number of replicas that must be in sync for a write with acks=all to be accepted. If fewer replicas are in sync, the leader rejects the write with NotEnoughReplicasException rather than accepting data it cannot safely replicate.
The canonical durable configuration is:
- Replication factor = 3 — three copies of every partition.
- min.insync.replicas = 2 — at least two copies must acknowledge each write.
- acks = all — the producer waits for those acknowledgements.
This tolerates the loss of one broker with no data loss and no downtime: writes still succeed against the remaining two replicas. If a second broker is lost, writes fail fast (correctly preferring availability of consistency over silently accepting unreplicated data), while reads continue.
RF=3, min.insync.replicas=2, acks=all
ISR = {leader, follower-1, follower-2} -> writes OK (3 >= 2)
one broker down: ISR = {leader, follower} -> writes OK (2 >= 2)
two brokers down: ISR = {leader} -> writes REJECTED (1 < 2)
Set min.insync.replicas on the topic so it travels with the data:
kafka-topics.sh --create \
--bootstrap-server localhost:9092 \
--topic payments \
--partitions 6 \
--replication-factor 3 \
--config min.insync.replicas=2
Producer configuration
Plain kafka-clients producer tuned for durability:
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"); // wait for all in-sync replicas
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); // no duplicates on retry
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);
try (Producer<String, String> producer = new KafkaProducer<>(props)) {
RecordMetadata md = producer
.send(new ProducerRecord<>("payments", "order-42", "{\"amount\":1999}"))
.get(); // block to surface failures
System.out.printf("acked at partition %d offset %d%n", md.partition(), md.offset());
}
Output:
acked at partition 3 offset 10472
The same settings in a Spring Boot 3.x application.yaml:
spring:
kafka:
producer:
bootstrap-servers: localhost:9092
acks: all
retries: 2147483647
properties:
enable.idempotence: true
max.in.flight.requests.per.connection: 5
Tip: Enabling idempotence implicitly requires
acks=alland constrainsmax.in.flight.requests.per.connectionto 5 or fewer. Spring for Apache Kafka and the modern client set these defaults for you, but making them explicit documents your durability intent for the next engineer.
Best Practices
- Use
acks=allfor any data you cannot afford to lose (payments, orders, audit events); reserveacks=1oracks=0for high-volume, lossy telemetry like metrics or logs. - Pair
acks=allwithmin.insync.replicas=2and replication factor 3 —acks=allis meaningless without an ISR floor. - Set
min.insync.replicasas a topic config, not a broker default, so the guarantee follows the topic across clusters. - Enable the idempotent producer (
enable.idempotence=true) so retries fromacks=alldo not create duplicates. - Always inspect the send result (block on
Future.get()or use an async callback) — never assume a send succeeded, especially with retries enabled. - Keep
min.insync.replicasstrictly less than the replication factor (2 < 3) so you can lose a broker without halting writes.