When a Kafka alert fires at 02:00, nobody has time to reread the design paper. What we need is a short, accurate model of what the cluster is doing so we can map a symptom ("producer timeouts", "lag on two partitions only", "consumers idle after deploy") to a mechanism and then to a command that confirms it. Our platform team runs a handful of KRaft clusters on Kafka 4.1 for a B2B invoicing product, and this is the model we hand new on-call engineers.

The post walks through the storage layout, replication and the in-sync replica set, what acks really promises, and how consumer groups split work. Each section ends with the commands we actually run.

Topics are names, partitions are the real unit

A topic is just a label. Everything that matters operationally happens per partition: a partition is an ordered, append-only log stored as segment files on one broker's disk, with a copy on each replica broker. Ordering is guaranteed only inside a partition. Parallelism is capped by the partition count, because inside one consumer group a partition is read by at most one member at a time.

On disk, a partition directory looks like this:

ls /var/lib/kafka/data/invoices.events-3/
# 00000000000004182733.index
# 00000000000004182733.log
# 00000000000004182733.timeindex
# 00000000000004519021.index
# 00000000000004519021.log
# 00000000000004519021.timeindex
# leader-epoch-checkpoint
# partition.metadata

The number in each file name is the base offset of that segment. Retention deletes whole segments, never single records, which is why a topic with retention.ms=86400000 can still hold data older than a day until the active segment rolls.

Debugging consequences we keep in mind:

  • A hot key lands every record on one partition. Lag on one or two partitions while the rest are at zero almost always means skewed keys, not slow consumers.
  • Changing the partition count changes which partition a key hashes to. That has ordering consequences and deserves its own runbook.
  • Disk usage per broker follows partition placement, not topic size, so one broker can fill up while the others are fine.

Leaders, followers and the ISR

Each partition has one leader replica and zero or more followers. Producers write to the leader; followers fetch from the leader exactly like consumers do. The leader tracks which followers are caught up. That set, including the leader itself, is the in-sync replica set (ISR).

A follower drops out of the ISR when it has not caught up to the leader's log end within replica.lag.time.max.ms (30 seconds by default). It rejoins when it catches up again. In KRaft mode the controller quorum, not ZooKeeper, records the ISR and elects new leaders; ZooKeeper support is gone entirely in the 4.x line.

The high watermark is the offset up to which all ISR members have the data. Consumers can only read below the high watermark. This matters during incidents: if a follower in the ISR is slow, the high watermark stops advancing, and consumers appear "stuck" even though the leader keeps accepting writes.

We check ISR health with:

kafka-topics.sh --bootstrap-server broker-1.example.com:9092 \
  --describe --under-replicated-partitions

kafka-topics.sh --bootstrap-server broker-1.example.com:9092 \
  --describe --topic invoices.events
# Topic: invoices.events  Partition: 3  Leader: 2  Replicas: 2,4,1  Isr: 2,4

Replicas is the assignment, Isr is reality. A partition listed with fewer ISR members than replicas is under-replicated. If the missing broker id is the same across many partitions, the problem is that broker (disk, GC, network), not the topic.

Two broker settings decide what happens when replicas fall behind:

Setting What it controls Our value
min.insync.replicas Minimum ISR size for a write with acks=all to succeed 2 (with replication factor 3)
unclean.leader.election.enable Whether an out-of-sync replica may become leader false

With unclean election disabled, a partition whose ISR is empty has no leader and goes offline rather than silently losing committed data. That is the right default for anything financial.

What acks actually promises

The producer acks setting decides when the leader answers a produce request:

acks Leader responds when Data loss window
0 Never waits Anything in flight
1 Written to leader log Records not yet copied when the leader dies
all (or -1) All current ISR members have it None, as long as ISR size stays at or above min.insync.replicas

Two details trip people up. First, acks=all means "all members of the current ISR", which could be just the leader if the followers have dropped out. That is why min.insync.replicas=2 matters: below it, the leader rejects writes with NotEnoughReplicasException instead of accepting them with weaker durability. Second, since Kafka 3.0 the Java producer defaults to acks=all and enable.idempotence=true, so many teams have the safe setting without knowing it, and then get confused when ISR shrinkage turns into producer errors.

When producers log this:

org.apache.kafka.common.errors.NotEnoughReplicasException: Messages are rejected since there are fewer in-sync replicas than required.

the producer is behaving correctly. The fix is on the broker side: find out why followers left the ISR.

Idempotence is a separate guarantee. With enable.idempotence=true the broker tracks a producer id and per-partition sequence numbers, so a retried batch is not written twice. It removes duplicates caused by producer retries; it does nothing about a consumer processing the same record twice.

Consumer groups and assignment

A consumer group is a set of consumers sharing a group.id. The group coordinator (one broker) assigns partitions to members and stores committed offsets in the internal __consumer_offsets topic. Since 4.0, the new consumer rebalance protocol from KIP-848 is generally available: with group.protocol=consumer, the broker computes assignments incrementally and a single member joining no longer stops the whole group. Classic groups still work and are what most existing clients use.

Rules we rely on when reading lag:

  • Consumers beyond the partition count sit idle. Twelve partitions and twenty pods means eight pods doing nothing.
  • Lag is per partition: log end offset minus committed offset. A committed offset is the next record to read, not the last one processed.
  • Committed offsets only move when the application commits (or auto-commit runs during poll()), so a consumer that processes records but crashes before committing will reread them.

The command we run first:

kafka-consumer-groups.sh --bootstrap-server broker-1.example.com:9092 \
  --describe --group invoice-indexer

# GROUP           TOPIC            PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG    CONSUMER-ID       HOST
# invoice-indexer invoices.events  0          881204          881210          6      indexer-7f9c...   /198.51.100.21
# invoice-indexer invoices.events  3          4519020         4602117         83097  indexer-2b11...   /198.51.100.24

If one partition has most of the lag, look at key skew or one slow consumer instance (the HOST column tells you which pod). If every partition lags evenly, the group as a whole is under-provisioned or a downstream dependency is slow. If CONSUMER-ID is empty, nobody owns that partition right now, which usually means a rebalance is in progress or the group is empty.

Reading common symptoms

Symptom Likely mechanism First check
Producer NotEnoughReplicasException ISR below min.insync.replicas --under-replicated-partitions, broker disk and GC
Producer TimeoutException on send Leader unreachable or overloaded, or metadata stale Broker request latency, leader distribution
Consumers stuck, leader still writing High watermark not advancing because of a slow ISR follower Follower fetch latency, replica.lag.time.max.ms
Lag on a few partitions only Key skew or one slow pod Per-partition lag, key distribution
Lag on all partitions Consumer throughput below produce rate Processing time per record, downstream latency
Partition offline ISR empty and unclean election disabled Which brokers hold the replicas, their health

Broker metrics worth a dashboard panel each: UnderReplicatedPartitions, OfflinePartitionsCount, ActiveControllerCount (exactly one across the controller quorum), ISR shrink and expand rates, and request handler idle ratio. A steady stream of ISR shrinks and expands on one broker is the earliest warning we get, usually hours before producers see errors.

Leader balance

Leadership drifts over time. After a broker restarts, its partitions get new leaders elsewhere, and unless preferred leader election runs, the restarted broker serves fewer leaders than it should while others carry extra load. auto.leader.rebalance.enable=true (the default) handles this periodically. When we need it immediately, after maintenance for example:

kafka-leader-election.sh --bootstrap-server broker-1.example.com:9092 \
  --election-type preferred --all-topic-partitions

Uneven leader counts show up as one broker with much higher network and request load. It looks like a hardware problem and is not.

Key takeaways

  • Think in partitions: ordering, parallelism, lag and disk usage are all per partition.
  • Replicas is the plan, Isr is what is actually in sync; compare the two first.
  • acks=all is only as strong as min.insync.replicas; producer errors from it are the safety net working.
  • Producer idempotence removes retry duplicates on the broker, not duplicate processing in consumers.
  • Consumers beyond the partition count are idle; lag on a few partitions points to skew, not capacity.
  • In Kafka 4.x the controller quorum owns metadata and ISR changes; there is no ZooKeeper to check.

References