Kafka Deep Dive¶
Prerequisites: Message Queue Patterns, Basic distributed systems
Why This Exists¶
Start with a simple problem: 1 producer → 1 topic → 1 consumer, 10 MB/s.
Works perfectly. Now: what happens at 500 MB/s?
A single consumer can't keep up. You need to parallelize consumption. But you can't just add random consumers — you'd process the same messages multiple times or miss messages.
This is why Kafka has partitions and consumer groups.
Mental Model¶
Topic "orders" with 3 partitions:
Producer ──┬──> Partition 0: [msg1, msg4, msg7, msg10...]
├──> Partition 1: [msg2, msg5, msg8, msg11...]
└──> Partition 2: [msg3, msg6, msg9, msg12...]
Consumer Group "order-processor":
Consumer C0 ──> Partition 0
Consumer C1 ──> Partition 1
Consumer C2 ──> Partition 2
Key rules: 1. Each partition is consumed by exactly one consumer within a group at a time 2. The Kafka log within a partition is an ordered append sequence 3. Messages across partitions have no global order 4. Consumer processing and end-to-end side effects are not automatic log-order guarantees 5. More consumers than partitions → some consumers are idle
Abstraction Levels¶
A distributed, ordered log split into partitions. Producers append; consumers read at their own pace, tracked by an offset. Log order exists only within a partition.
"Kafka gives you per-partition log order, at-least-once delivery, and horizontal scale via partitions and consumer groups." Typical interview shorthand: Kafka transactions can give exactly-once processing inside Kafka-centric consume/process/produce flows — not a universal end-to-end guarantee for databases, APIs, email, or payments.
Ordering is per-partition, not global — a naive partition key or a repartition changes which messages land together. "Exactly-once" is three separable layers — idempotent producer (dedupes retried appends), Kafka transactions (atomic consume-produce-commit inside Kafka), and everything downstream of Kafka (a DB write, an email, a payment) still needs its own idempotency, because Kafka cannot make an external side effect exactly-once by itself. Rebalancing also isn't one behavior: eager rebalancing stops the whole group; cooperative/incremental rebalancing (2.4+) only reassigns the partitions that actually moved.
The moment your consumer does async or multi-threaded processing, retries, or writes to a dead-letter queue and replays later — Kafka's per-partition log order no longer implies your processing order. At that point ordering is a property you have to re-establish in your consumer, not something Kafka is still guaranteeing for you.
See Exactly-Once Semantics and Ordering Guarantees below for the full detail behind each claim above.
Run it yourself
The simulator above shows the mechanism; a 3-broker Kafka cluster you can actually kill lives in labs/kafka — create a real topic, kill a real broker, and watch a real leader election and consumer-group rebalance.
Architecture¶
graph LR
subgraph Producers
P1[Producer 1]
P2[Producer 2]
end
subgraph Topic["Topic: orders (3 partitions, RF=3)"]
subgraph Broker1["Broker 1 (Leader P0)"]
PA["P0: [0,1,2,3...]"]
end
subgraph Broker2["Broker 2 (Leader P1)"]
PB["P1: [0,1,2,3...]"]
end
subgraph Broker3["Broker 3 (Leader P2)"]
PC["P2: [0,1,2,3...]"]
end
end
subgraph CG["Consumer Group: order-processor"]
C0["C0\nOffset: 42"]
C1["C1\nOffset: 38"]
C2["C2\nOffset: 45"]
end
P1 -->|key hash| PA
P1 -->|key hash| PB
P2 -->|key hash| PC
PA --> C0
PB --> C1
PC --> C2 Interactive Kafka Simulation¶
Try: 1. Start producer → observe lag stays near 0 when consumers keep up 2. Kill a consumer → watch lag build up, observe rebalancing 3. Add a partition → can increase throughput but note the rebalancing cost
How It Works Internally¶
Offsets¶
Every message in a partition has a monotonically increasing offset. Kafka's committed-offset convention is the offset of the next record the consumer should read, not the offset of the last one it processed — so after successfully processing the message at offset 5, the consumer commits 6, and a restart resumes from 6, not 5 (committing 5 would mean re-reading and reprocessing that same message on restart).
Partition 0: offset 0, 1, 2, 3, 4, 5, 6...
↑
Consumer just processed offset 5
→ commits 6 (next record to consume)
→ on restart, resume from 6
Auto-commit pitfall: Kafka can auto-commit the offset before the message is actually processed. If the consumer crashes between auto-commit and processing → message loss.
Manual commit after processing → at-least-once semantics (safe: possible duplicates, no losses).
Rebalancing¶
When a consumer joins or leaves a group, Kafka triggers a rebalance and the group coordinator redistributes partitions.
Eager rebalances stop group consumption while assignments are revoked and redistributed (stop-the-world for the group). Cooperative/incremental rebalancing (Kafka 2.4+) reduces disruption by retaining unaffected assignments and moving partitions incrementally — consumers keep consuming partitions they still own.
Consumer Lag¶
Consumer lag = (Latest offset in partition) − (Committed offset of consumer)
High lag indicates: - Consumer is too slow (processing bottleneck) - Consumer is down - Traffic spike outpacing consumption rate
ISR (In-Sync Replicas)¶
Each partition has one leader and N-1 followers (replicas). The ISR is the set of replicas fully caught up with the leader.
acks=all(strongest): producer waits for every current ISR member to acknowledge — notmin.insync.replicasreplicas.min.insync.replicasis a floor: if|ISR| < min.insync.replicas, the produce fails (NotEnoughReplicas)acks=1: only the leader acknowledges (independent ofmin.insync.replicas; risk: leader dies before replication = data loss)acks=0: fire and forget (highest throughput, data loss possible)
Consumers default to fetching from the leader. Since KIP-392 they can fetch from the closest replica (replica.selector.class / client.rack) — "always from the leader" is the old default, not a hard rule.
Ordering Guarantees¶
| Scope | Guarantee |
|---|---|
| Kafka log within a partition | Ordered append/log sequence |
| Across Kafka partitions | No global order |
| Consumer processing | Depends on execution model |
| End-to-end side effects | Depends on retries, parallelism and downstream system |
How to keep related messages in the same Kafka log sequence: Use the same partition key. That gives per-partition log order, not an automatic end-to-end processing guarantee.
# All events for user 123 go to the same partition
producer.send(
topic="user-events",
key=b"user:123", # same key → same partition
value=event_payload
)
Pitfall: If user 123 is extremely active, their partition becomes a hot partition — one consumer processes 10× more messages than others.
Failure Modes¶
Hot Partition¶
- Cause: Highly skewed key distribution (one key generates most messages)
- Symptoms: One partition's lag grows while others are fine; one consumer at 100% CPU
- Detection: Per-partition message rate; consumer lag by partition
- Fix: Add random suffix to hot key (
user:123:0,user:123:1), or use null key (round-robin)
Consumer Rebalance Loop¶
- Cause: Consumer takes too long to process → exceeds
max.poll.interval.ms→ Kafka assumes it's dead → rebalances → same consumer rejoins → repeat - Symptoms: Frequent rebalances in logs, lag oscillates up and down
- Detection: Group coordinator logs, rebalance frequency metric
- Fix: Increase
max.poll.interval.msOR reducemax.poll.records(process fewer messages per poll) OR optimize consumer processing
Poison Message¶
- Cause: A malformed message causes consumer to crash on every attempt
- Symptoms: Consumer restarts repeatedly, lag on specific partition never decreases
- Detection: Consumer crash logs with same offset repeatedly
- Fix: Dead Letter Queue (DLQ) — after N retries, move to DLQ topic; alerting on DLQ messages
Unclean Leader Election¶
- Cause: All ISR replicas are down; Kafka elects an out-of-sync replica as leader (
unclean.leader.election.enable=true) - Impact: Data loss — messages produced after this replica's last sync are gone
- Fix: Set
unclean.leader.election.enable=falsefor durability-critical topics
Production Debugging¶
Symptom: Consumer lag growing steadily
Diagnostic steps:
1. Check consumer CPU/memory (is processing bottlenecked?)
→ JVM GC pauses? Processing logic slow?
2. Check if consumer is actually consuming
→ Consumer group offset progress over time
→ kafka-consumer-groups.sh --describe
3. Check for rebalances
→ group coordinator logs, rebalance metric
4. Check partition count vs consumer count
→ More partitions than consumers = parallelism bottleneck
5. Check producer throughput increase
→ Topic ingestion rate vs consumer throughput rate
6. Check for poison messages
→ Consumer crash logs, specific partition stuck at same offset
Key metrics:
- consumer_lag_messages (per partition)
- consumer_group_rebalance_count
- broker_network_io, broker_disk_io
- request_handler_avg_idle_percent (< 30% = broker overloaded)
- under_replicated_partitions (> 0 = replication issue)
Scaling Limits¶
- Partition count is permanent — can increase, cannot decrease (without data migration)
- Each partition is a file on disk — too many partitions → too many file handles, slower leader election
- Rule of thumb: < 10,000 partitions per broker
- Consumer lag recovery: scale out consumers (up to partition count)
- Write throughput: scale out by adding partitions and brokers
- Retention: controlled by
retention.bytesandretention.ms
Trade-offs¶
| Decision | Option A | Option B | Consideration |
|---|---|---|---|
acks setting | all (durable) | 1 or 0 (fast) | Durability vs throughput |
| Partition count | More (parallelism) | Fewer (simpler) | Throughput vs operational complexity |
| Consumer commit | Manual (safe) | Auto (simple) | At-least-once vs potential duplicates |
| Retention | Long (replay) | Short (cost) | Debuggability vs cost |
| Compaction | Yes (latest per key) | No (time-based) | Lookup use cases vs streaming |
Interview Questions¶
Q: How does Kafka ensure a message is processed exactly once?
"Kafka transactions provide exactly-once processing semantics for Kafka-centric consume/process/produce flows by atomically coordinating consumed offsets and output records (sendOffsetsToTransaction + transactional produce; downstream isolation.level=read_committed). Kafka Streams additionally folds changelog/state-store updates into that transaction — that layer is Streams-specific, not a required part of the producer API. External database/API/email/payment side effects still require their own idempotency or transactional integration. For most use cases, at-least-once with idempotent consumers is simpler and sufficient — for example, an upsert by message ID rather than a blind insert."
Q: How do you handle a hot partition in Kafka?
"First, identify the hot key — look at per-partition message rates to see which partition is receiving disproportionate traffic. If it's a specific key (e.g., one large tenant), options are: (1) add a random suffix to the key to distribute across multiple partitions at the cost of losing ordering; (2) create a separate topic for that tenant; (3) handle the hot key at the application level — deduplicate or aggregate before producing. One thing that does NOT help: creating a separate consumer group for that partition — a new consumer group gets its own independent copy of the entire topic's data (every group tracks its own offsets and reads everything), it doesn't add processing capacity to a specific partition within the original group. Since exactly one consumer within a group can read a given partition at a time, the only way to add parallelism to a single hot partition without splitting the key itself is to split that partition's data across more partitions (options 1/2 above) — you can't just throw more consumers at one partition within the same group, and a second group duplicates work rather than sharing it."
Q: We're migrating from 3 to 30 partitions for a critical topic. What are the risks?
"Key risk: increasing partition count triggers a consumer group rebalance. Eager rebalances stop group consumption while assignments are revoked and redistributed; cooperative/incremental rebalancing reduces that disruption but the group still has to move partitions. For a critical topic this could mean seconds to minutes of lag buildup depending on protocol and data volume. Plan: (1) schedule during low-traffic window; (2) ensure downstream consumers can handle the backlog after rebalance; (3) understand that log order is per-partition — existing messages for a key may now be on a different partition than new messages. If you use key-based ordering, existing consumers processing an old partition will interleave with new producers writing to the new partition for the same key. This is an architectural risk for order-sensitive workflows. I'd also validate that all consumer configurations (max.poll.records, session.timeout) are tuned for the new throughput per consumer."
Key Takeaways¶
Remember
- Partitions are the unit of parallelism — more partitions = more consumers can run in parallel
- Each partition is consumed by exactly one consumer per group at a time
- The Kafka log is ordered only within a partition — use partition keys for related messages. Consumer processing and downstream side effects are not automatic log-order guarantees.
- Consumer lag = latest_offset − committed_offset. The rate difference (produce − consume) is d(lag)/dt, not lag itself. High lag = consumer behind.
- Eager rebalances stop group consumption while assignments are revoked and redistributed. Cooperative/incremental rebalancing reduces disruption by retaining unaffected assignments. Tune
max.poll.interval.msto avoid unnecessary rebalances. - Hot partitions require application-level fixes, not just Kafka configuration
Version taught
Version taught / last verified: 2026-08 (partitions, consumer groups, eager vs cooperative rebalance, ISR as a concept). Current upstream: Apache Kafka 4.3.1 (latest GA as of 2026-06; 4.4 was still RC in 2026-08). Compatibility: group parallelism = partitions still holds. KRaft is the default metadata path on 4.x — this page does not teach ZooKeeper-based controllers. Cooperative/incremental rebalancing is 2.4+; do not assume every cluster has it enabled.