Siddhant Deval
Siddhant Deval
backend18 min read

Kafka Internals: Partitions, Leaders, and the Commit Log

Kafka's throughput and ordering guarantees both derive from the same mechanism — partition-scoped, append-only commit logs with leader-follower ISR replication. Understanding this single design decision makes every Kafka configuration choice obvious: acks, min.insync.replicas, partition key design, log compaction, and cooperative rebalancing.

Kafka Internals: Partitions, Leaders, and the Commit Log

The senior engineer on call at 3 a.m. sees the alert: Kafka consumer lag on payments.processed has climbed to 4.2 million messages and is growing. They increase the consumer group from 6 instances to 12. Lag continues climbing. The reason is that the topic has 6 partitions. Adding more consumer instances beyond the partition count does nothing — the extras sit idle. Kafka's consumer model is not a thread pool; it is a partition assignment. Understanding why requires going one level deeper than the API.

A message is a fact about the world — and Kafka's design encodes one specific, deliberate answer to the question of where that fact lives and who may read it.

Architectural Note

Series positioning: This is Part 3 of Distributed Messaging Systems. Part 2 established the queue vs log distinction and the offset model. This article goes below the offset to the physical storage and replication layer. The existing Kafka Deep Dive (Distributed Architecture series) covers the foundational commit log model — this article assumes that baseline and focuses on the KRaft controller, acks matrix, cooperative rebalancing, and log compaction. Part 4 is the parallel RabbitMQ deep-dive.


1. The Partition: Kafka's Unit of Everything

1.1 One Partition, One Append-Only File

Every Kafka topic is divided into N partitions. A partition is not an abstraction — it is a directory on the broker's filesystem containing sequential segment files:

TEXT
/var/kafka/data/payments.processed-0/
  00000000000000000000.log      ← segment 0: offsets 0–999,999
  00000000000000000000.index    ← sparse offset index (offset → file byte position)
  00000000000000000000.timeindex← sparse timestamp index
  00000000000001000000.log      ← segment 1: offsets 1,000,000–1,999,999
  00000000000001000000.index
  leader-epoch-checkpoint       ← monotonic counter incremented on leader change

Producers append to the active segment at the tail. Consumers read from any segment by offset lookup. The broker never modifies written data — the log is immutable. This is what makes Kafka fast: sequential disk writes use the OS page cache optimally, and sendfile(2) zero-copy transfers data directly from page cache to the network socket without copying through userspace.

Pro Tip & Optimization

Kafka's throughput comes from two OS mechanisms: sequential I/O (appending to one file end) and zero-copy (sendfile syscall bypasses userspace copy). If your Kafka brokers are slow, check whether the page cache is being evicted — brokers with insufficient RAM force disk reads on every consumer fetch.

1.2 Partition Key and Ordering Guarantees

Kafka guarantees ordering within a partition, not across partitions. The partition a message lands on is determined by its key:

TYPESCRIPT
// ❌ No key — round-robin distribution, no ordering guarantee
await producer.send({
  topic: 'payments.processed',
  messages: [{ value: JSON.stringify({ paymentId: 'P1', status: 'completed' }) }]
})

// ✅ Key = partition determinism + ordering within a payment's lifecycle
await producer.send({
  topic: 'payments.processed',
  messages: [{
    key:   'payment:P1',   // hash(key) % numPartitions = same partition every time
    value: JSON.stringify({ paymentId: 'P1', status: 'completed' }),
  }]
})
// payment:P1 always lands on partition 3
// payment:P2 always lands on partition 1
// All events for P1 arrive at consumer in the order they were written
Crucial Requirement

Ordering is scoped to a partition key, not a topic. Two events for the same payment ID are guaranteed to be ordered. Two events for different payment IDs on different partitions are not ordered relative to each other — and cannot be made so without a single-partition topic (which eliminates parallelism).

Partition Key Design Ordering Scope Parallelism
No key (null) None — round-robin Maximum (all partitions used)
Entity ID (payment:P1) Per entity High (events per entity ordered; entities parallel)
Tenant ID Per tenant Medium (all tenant events ordered)
Constant key Total ordering None (single partition = single consumer)

2. Replication: ISR and the acks Matrix

2.1 Leader-Follower Replication

Each partition has one leader broker and N-1 follower brokers (N = replication factor). All producer writes go to the leader. Followers fetch from the leader continuously, maintaining an In-Sync Replica (ISR) list — the set of followers that are caught up within replica.lag.time.max.ms (default 30s).

2.2 The acks Matrix: Durability vs Latency

TYPESCRIPT
// ❌ acks=0 — fire and forget. No durability. Use only for metrics you can afford to lose.
const producer = kafka.producer({ acks: 0 })

// ❌ acks=1 (default) — leader writes to local disk, acks immediately.
// Risk: leader crashes before follower fetches → message lost permanently.
const producer = kafka.producer({ acks: 1 })

// ✅ acks=all (-1) — leader waits for ALL in-sync replicas to write before acking.
// Combined with min.insync.replicas=2: tolerates one broker failure with zero data loss.
const producer = kafka.producer({ acks: -1 })
acks min.insync.replicas Durability Latency impact
0 N/A None — message may be lost even if broker is healthy Lowest (+0ms)
1 N/A Leader-only — lost if leader crashes before replication Low (+1–5ms)
all 1 Equivalent to acks=1 — ISR of 1 means only leader Medium
all 2 Safe under single broker failure Medium (+5–15ms)
all 3 Safe under two concurrent broker failures Higher (+15–30ms)
Performance / Safety Warning

acks=all without setting min.insync.replicas=2 provides false safety. If the ISR shrinks to 1 (followers fall behind), acks=all only requires the leader to confirm — identical to acks=1. Always set both together: acks=all + min.insync.replicas=2.


3. Consumer Groups and the Partition Assignment Problem

3.1 Why Max Parallelism = Partition Count

A partition can be assigned to at most one consumer within a group at any time. Six partitions, six consumers maximum. The seventh sits idle — this is why scaling consumer instances beyond partition count does nothing for throughput.

Crucial Requirement

If consumer lag is growing and you're already at the partition count ceiling: (1) increase the partition count on the topic, (2) scale consumers to match. Partition counts can be increased but never decreased — plan ahead. A common production setting is 12–48 partitions for high-throughput topics to give future scaling headroom.

3.2 Cooperative Rebalancing vs Eager Rebalancing

When a consumer joins or leaves a group, Kafka must reassign partitions — this is a rebalance. The legacy eager (stop-the-world) protocol revokes all partition assignments, pauses all consumers, then re-assigns. For a group of 20 consumers this can cause 10–30 seconds of processing pause.

The cooperative (incremental) rebalancing protocol, available since Kafka 2.4 with CooperativeStickyAssignor, only revokes partitions that need to move:

TYPESCRIPT
// ✅ Cooperative rebalancing — only affected partitions are revoked
const consumer = kafka.consumer({
  groupId: 'payments-service',
  partitionAssigners: [PartitionAssigners.roundRobin],
  // kafkajs uses CooperativeStickyAssignor when available in broker
  rebalanceTimeout: 60000,
})

consumer.on(consumer.events.REBALANCING, async () => {
  // With cooperative protocol: only revoked partitions pause
  // Unaffected partitions continue processing during rebalance
  metrics.increment('consumer.rebalance')
})
Protocol Partition revocation Pause during rebalance Use case
Eager (default pre-2.4) All partitions revoked All consumers pause Simple; safe with any assignor
Cooperative (incremental) Only moved partitions revoked Unaffected partitions continue Production; reduces downtime

4. KRaft: Kafka Without ZooKeeper

Since Kafka 3.3, the KRaft (Kafka Raft Metadata) mode replaces ZooKeeper as the controller. Understanding KRaft matters for cluster operations and failure recovery.

4.1 What Changed

ZooKeeper mode (pre-3.3) KRaft mode (3.3+)
Metadata store External ZooKeeper ensemble Built-in Raft log on controller nodes
Controller election ZooKeeper leader election Raft consensus among controller quorum
Partition count limit ~200K (ZooKeeper write bottleneck) Millions (Raft log throughput)
Operational complexity Two systems to operate Single system
Startup time Slow (ZooKeeper metadata load) Fast (local Raft log replay)
BASH
# KRaft cluster: controller nodes are separate from broker nodes (production)
# Controller quorum (odd number for Raft): 3 or 5 nodes
# Broker nodes: any number

# Check KRaft mode
kafka-metadata-quorum.sh --bootstrap-controller localhost:9093 describe --status
# Output: LeaderId, LeaderEpoch, HighWatermark, MaxFollowerLag
Architectural Note

ZooKeeper mode is fully deprecated as of Kafka 4.0. If you are deploying new clusters, use KRaft. If you are operating existing ZooKeeper clusters, the migration path is a rolling metadata migration using kafka-storage.sh — it does not require downtime on brokers.


5. Log Compaction: Keeping the Latest Fact Per Key

For topics where only the latest value per key matters (user profile, product inventory, account balance), Kafka's log compaction removes superseded records, keeping only the most recent message per key:

TYPESCRIPT
// ✅ Compacted topic: Kafka retains only the latest value per key
// Config: cleanup.policy=compact (instead of delete)

// These three publishes for the same user ID:
await producer.send({ topic: 'user-profiles', messages: [
  { key: 'user:42', value: JSON.stringify({ name: 'Alice', plan: 'free' }) },
  { key: 'user:42', value: JSON.stringify({ name: 'Alice', plan: 'pro' }) },   // plan upgrade
  { key: 'user:42', value: JSON.stringify({ name: 'Alice', plan: 'pro', mfa: true }) }, // MFA added
]})

// After compaction: only the third record for user:42 is retained
// New consumers starting from offset=0 still see the full current state of every key
// Tombstone (null value): deletes the key from the compacted log entirely
await producer.send({ topic: 'user-profiles', messages: [
  { key: 'user:42', value: null }  // tombstone — user deleted
]})
Pro Tip & Optimization

Log compaction is the correct pattern for building event-sourced read models (KTable in Kafka Streams, or any consumer that bootstraps by replaying the compacted log). Do not use time-based retention for topics that serve as the source of truth for a read model — compaction ensures new consumers always get the complete current state without reading years of history.


Summary

Concept Rule
Partition as storage unit Every partition is a single append-only file segment on disk; Kafka's throughput comes from sequential I/O and zero-copy OS transfers.
acks durability acks=all + min.insync.replicas=2 is the only configuration that prevents data loss under a single broker failure.
Partition key ordering Partition key determines ordering scope: all events with the same key are guaranteed to land on the same partition in the same order.

What's Next

Part 4: RabbitMQ & AMQP — Smart Routing, Dead Letters, and Quorum Queues is the parallel deep-dive on the queue model: exchange types, binding topologies, quorum queue durability, channel multiplexing, and the dead-letter pipeline. If you arrived from Part 2 as a Kafka-focused reader, Part 4 is optional — the series converges at Part 5 on delivery guarantees, which you can read directly.

Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#Kafka#Commit Log#Partitions#KRaft#ISR#Consumer Groups#Backend#Distributed Systems
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.