Kafka troubleshooting
Messages are lost after a broker failure
Kafka only promises that a committed record, one that every in-sync replica has, survives as long as one in-sync replica stays alive. Records are lost after a broker failure when they were acknowledged before that point (acks=0 or acks=1), when a replica outside the ISR became leader (unclean leader election), or when the ISR had already shrunk to a single broker. Produce with acks=all to topics with replication factor 3 and min.insync.replicas=2, and keep unclean leader election off.
Reproduce it in the playground3 steps beforeacks=1 - leader acks after a local write. t+2.500s
The producer is acknowledged ('ok') the moment the leader appends the record to its own log (LEO=1). The high watermark is still 0: no follower has replicated the record, and acks=1 never waited for them.
Open at this step: step 2, acks=1 - leader acks after a local write2 steps beforeThe gap: acked, but uncommitted. t+4.900s
The producer believes the write succeeded, yet the record lives on exactly one broker. HW is still 0, so the record is not yet below the high watermark and stays invisible to consumers - and one crash from being lost.
Open at this step: step 3, The gap: acked, but uncommitted1 step beforeLeader killed before replication. t+5.200s
broker-1 (the leader) is gone. The partition is leaderless and electing. The surviving ISR (broker-2, broker-3) never fetched the record - their LEO is 0.
Open at this step: step 4, Leader killed before replicationThe moment it happensNew leader at LEO 0 - record lost. t+6.000s
A new leader is elected from the surviving ISR at LEO 0. There is nothing to recover: the only copy was on the dead leader. The high watermark is still 0 and never advances.
Open at this step: step 5, New leader at LEO 0 - record lostRight afterConsumers never saw it. t+8.000s
The consumer has polled repeatedly and read nothing past offset 0 - the acknowledged record is permanently lost from every consumer. acks=all (with min.ISR=2) would have held the ack until every in-sync replica, all three here, had the record.
Open at this step: step 6, Consumers never saw itWhat it means
A record becomes committed once every replica in the ISR has it, and only committed records are given to consumers. When a leader fails, the controller picks the new leader from the ISR, so the new leader has every committed record. When the old leader comes back as a follower, it truncates whatever the new leader does not have before it fetches again, and logs "Truncating to offset 120" for the partition. Whether your producer was told about such a record decides whether it was lost or simply retried: acks=all is acknowledged only once the record is committed, acks=1 as soon as the leader has written it, and acks=0 never. Since Kafka 3.0 producers default to acks=all with idempotence on, but the broker defaults still allow loss: min.insync.replicas=1 lets acks=all succeed on the leader alone, and default.replication.factor=1 creates single-copy topics. Most loss comes from those defaults left in place, from producer settings or topic overrides someone changed, or from applications that ignore failed sends.
Likely causes, most common first
- The producer used acks=1 or acks=0. With acks=1 the leader acknowledges after writing to its own log. If it dies before a follower fetches the record, the new leader never had it. acks=0 does not wait at all, and the producer is never told. min.insync.replicas does not protect either one: it is only checked for acks=all.
- Unclean leader election promoted a stale replica. With unclean.leader.election.enable=true, on the broker or as a topic override, a replica outside the ISR can become leader when no in-sync replica is available. Its log becomes the source of truth, and records it never received are gone for good, including ones acks=all producers were told had been committed. The default is false.
- The ISR had shrunk to the leader alone. acks=all waits for the current ISR, not for the full replica list. With min.insync.replicas=1, which is the broker default, a partition whose followers have fallen out keeps acknowledging writes that exist on one broker, and losing that broker loses them.
- All replicas failed together. Replicas placed in one rack, zone or power domain can go down at the same time. Without broker.rack set, replica placement does not take that into account.
- The send failed and nobody looked. send() is asynchronous. If a record cannot be delivered within delivery.timeout.ms (2 minutes by default), the error arrives in the callback or the returned Future, and code that checks neither drops the record without a trace.
How to fix it
- Produce with acks=all (the default since Kafka 3.0) to topics with replication factor 3 and min.insync.replicas=2, so every acknowledged record exists on at least two brokers.
- Keep unclean.leader.election.enable=false on the brokers and check topic overrides with kafka-configs.sh --describe. Alert on the controller metric UncleanLeaderElectionsPerSec, which should stay at 0.
- Handle send failures in the producer callback: log them, alert on them, and stop or retry deliberately instead of carrying on.
- Set broker.rack on every broker so the replicas of each partition land in different racks or zones.
What not to do
- Enabling unclean leader election to bring a partition back online without deciding that losing its recent records is acceptable. If that is the decision, run it for the one partition with kafka-leader-election.sh --election-type unclean rather than turning it on for the whole cluster.
Configs involved
acksHow many replicas must have a write before the producer counts it as sent: 0 (none), 1 (the leader only) or all (every in-sync replica, the default, and required for idempotence).min.The fewest in-sync replicas, leader included, that must have a write for an acks=all produce to succeed (default 1); with fewer in the ISR the leader rejects the write with NOT_ENOUGH_REPLICAS.insync. replicas unclean.Lets a replica that is not in the ISR become leader when no in-sync replica is left, which brings the partition back online but loses the writes that replica never received (default false).leader. election. enable enable.Gives the producer an id and a sequence number per partition so the broker drops duplicate retries and keeps them in order; on by default unless a conflicting setting (acks other than all, retries=0, more than 5 in-flight requests) turns it off.idempotence delivery.The upper bound on reporting success or failure after send() returns, covering the wait in a batch, the wait for the acknowledgement and every retry (default 120000, 2 minutes); it should be at least request.timeout.ms plus linger.ms.timeout. ms broker.The rack or availability zone of the broker; when a topic is created, the replicas of each partition are spread across racks so one rack failing cannot take every copy.rack
Reproduce it in the simulator
- Kafka acks=1 data loss: min.insync.replicas does not help (acks=1 durability gap) Open at the moment it happens: Kafka acks=1 data loss: min.insync.replicas does not help
- Kafka producer acks=0: fire and forget, silent data loss (acks=0: fire and forget) Open at the moment it happens: Kafka producer acks=0: fire and forget, silent data loss
- Kafka unclean leader election: permanent loss of acked data (Unclean election: permanent data loss) Open at the moment it happens: Kafka unclean leader election: permanent loss of acked data
Sources
- Apache Kafka 4.3: producer config acks
- Apache Kafka 4.3: topic config min.insync.replicas
- Apache Kafka 4.3: topic config unclean.leader.election.enable
- Apache Kafka 4.3: design, replication and committed messages
- Apache Kafka 4.3: design, availability and durability guarantees
- Apache Kafka 4.3: broker config default.replication.factor (default 1)
- Apache Kafka 4.3: producer config delivery.timeout.ms
- Apache Kafka 4.3: balancing replicas across racks
- Apache Kafka 4.3: monitoring (UncleanLeaderElectionsPerSec)
- KIP-101: truncation by leader epoch
- KIP-679: producer enables the strongest delivery guarantee by default
- Apache Kafka 4.3.0 source: UnifiedLog.java (Truncating to offset)