Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Beginner+100 XP

Producers, Consumers, and Brokers

How Kafka clients and servers actually work together: metadata discovery, broker leadership, producer batching and acks, the consumer poll loop, pull-based backpressure, and cluster coordination.

60 min September 2026
// Part 01 — The three roles

Kafka Has Writers, Readers, and Servers

Every Kafka system has three major roles. Producers write records. Consumers read records. Brokers are Kafka servers that store records, replicate partitions, serve client requests, and coordinate cluster metadata. If you understand how these three roles interact, the rest of Kafka becomes much easier to debug.

A producer does not write to "Kafka" as an abstract cloud. It writes to a specific broker that is currently the leader for a specific partition. A consumer does not receive magical pushes from Kafka. It fetches records from brokers. A broker is not just a network proxy. It stores log segments on disk, handles replication, enforces security, exposes metadata, and participates in cluster leadership.

The reason this module matters more than it looks is that almost every production Kafka incident traces back to a misunderstanding of one of these three roles. "Why did we lose a message?" is almost always a producer acknowledgement question. "Why is processing duplicated?" is almost always a consumer offset-commit-timing question. "Why did clients suddenly time out?" is almost always a broker leadership or metadata question. Learning the mechanics up front means you debug real incidents by reasoning instead of guessing.

Beginner model: producer sends, broker stores, consumer reads.

Production model: producer discovers metadata, chooses a partition, batches and sends to the partition leader, waits for acknowledgements; brokers append and replicate records and coordinate cluster state through a controller; consumers poll assigned partitions inside a bounded loop, process records, and commit offsets only when the work is actually safe to mark done.

Why this ordering
This module walks broker responsibilities first, then producers, then consumers, because clients cannot be understood in isolation. A producer's acks setting only makes sense once you know what a partition leader and an in-sync replica are. A consumer's offset commit strategy only makes sense once you know that Kafka retains records instead of deleting them on read.
RoleInitiates connections?Stateful?Typical deployment shape
ProducerYes — connects out to brokers to send data.Mostly stateless; buffers unsent batches in memory only briefly.Embedded inside an application or service, not run as its own standalone process.
BrokerNo — accepts inbound connections from clients and other brokers.Highly stateful — owns disk-resident partition logs and replication state.Run as a dedicated, long-lived server process, usually one per physical or virtual machine.
ConsumerYes — connects out to brokers to fetch data.Stateful in the sense of tracking offsets, but that state lives in Kafka, not the process itself.Embedded inside an application or run as a dedicated consuming service, often scaled out as a group.
// Part 02 — Brokers

A Broker Is the Kafka Server, But It Has Many Jobs

A broker is one Kafka server process. A cluster is a group of brokers. In a production Kafka cluster, each broker usually leads some partitions and follows others. Leadership is per partition, not per topic and not per cluster. Broker 1 might lead orders partition 0, broker 2 might lead orders partition 1, and broker 3 might lead payments partition 0.

Broker responsibilityWhat it means
Store partition logsBrokers write topic partition data to disk as log segments.
Serve producer writesThe leader broker for a partition receives writes for that partition.
Serve consumer readsConsumers fetch records from brokers that host the partition leaders.
Replicate dataFollower replicas copy data from partition leaders.
Expose metadataClients ask brokers which topics exist, how many partitions they have, and who leads them.
Enforce securityBrokers authenticate clients and authorize operations when security is enabled.
Participate in cluster coordinationBrokers elect and follow a controller that manages partition leadership across the whole cluster.

Bootstrap servers are only the front door

Kafka clients have a setting called bootstrap.servers. Beginners often think this must list every broker. It does not. It only needs enough reachable brokers for the client to connect and discover cluster metadata. After that, the client learns which brokers lead which partitions and talks to the right brokers directly.

Bootstrap discovery
client config:
  bootstrap.servers=broker-1:9092,broker-2:9092

client connects to broker-1
broker-1 returns metadata:
  orders partition 0 leader = broker-3
  orders partition 1 leader = broker-2
  payments partition 0 leader = broker-1

client sends each request to the correct leader

In production, teams list two or three brokers in bootstrap.servers, not all of them, specifically so that a single broker being down at startup does not prevent the client from connecting. Listing every broker is not wrong, it is simply unnecessary — the client only needs one live entry point to bootstrap the rest of its metadata.

ScenarioDoes bootstrap.servers need updating?
A brand new broker joins the clusterNo — existing clients discover it through metadata the next time they refresh, without any config change.
One of the listed bootstrap brokers is permanently decommissionedOnly if every listed broker is removed at once; as long as at least one listed entry stays reachable, the client keeps working.
The entire cluster is migrated to new hosts with new addressesYes — bootstrap.servers is the one hardcoded entry point and must point somewhere reachable.

The controller broker: who manages the cluster itself

A Kafka cluster needs one broker to make cluster-wide decisions: which broker becomes the new leader when a partition's current leader fails, which brokers are allowed in the in-sync replica set, and how topic and partition metadata changes propagate. That broker is called the controller. There is exactly one active controller per cluster at any time.

Historically, Kafka used an external coordination service called ZooKeeper to elect the controller and store cluster metadata. As of Kafka 3.x and later, clusters run in KRaft mode(Kafka Raft), where a small set of dedicated controller nodes replicate cluster metadata among themselves using the Raft consensus protocol, and Kafka no longer depends on ZooKeeper at all. New clusters you stand up today, including local development clusters, should run KRaft mode — it has fewer moving parts, no separate service to operate, and faster controller failover.

ModeHow the controller is chosenStatus
ZooKeeper mode (legacy)ZooKeeper ensemble elects the controller broker via an ephemeral lock.Deprecated; removed in newer Kafka major versions. Only seen in older, unmigrated clusters.
KRaft mode (current)A quorum of controller nodes elects a leader among themselves using Raft, independent of any external service.Default and recommended for all new clusters, including single-broker local setups.
You do not need to run ZooKeeper
If you are setting up Kafka for the first time in 2026, you never need to install or configure ZooKeeper. Every current Kafka distribution supports KRaft mode out of the box, including single-node setups where one process acts as both broker and controller. Module 04 walks through exactly this kind of setup.

What the controller actually does, mechanically: when a broker hosting a partition leader crashes or is shut down, the controller detects this (brokers send periodic heartbeats to the controller quorum), picks a suitable replacement leader from the partition's in-sync replicas, and pushes that new leadership assignment out to every broker in the cluster. Producers and consumers learn about the change the next time they refresh metadata, which is exactly what Part 09 of this module covers in depth.

Why a dedicated controller role, instead of every broker deciding independently

It might seem simpler for each broker to independently detect a failed peer and just start serving whichever partitions it thinks are now leaderless. This breaks down immediately in any real network: two brokers might both detect the same failure at slightly different times and both decide to become leader for the same partition, a scenario called split-brain. A single, agreed-upon controller avoids this entirely by making leadership decisions in one place and propagating them consistently, so every broker in the cluster converges on the same view of who leads what, rather than each broker guessing independently and possibly disagreeing.

// Part 03 — Producers

Producers Write Records, But Safe Writing Has Many Steps

A producer is any application that writes records to Kafka. It can be a backend service, data connector, mobile ingestion API, log shipper, IoT gateway, or stream processor. The producer's job is not merely to "send JSON." It must serialize records, choose partitions, batch records, retry temporary failures, and decide what acknowledgement is enough for a write to count as successful.

  • Serialize the key and value into bytes.
  • Fetch metadata so it knows partition leaders.
  • Choose a partition using the key or partitioning strategy.
  • Batch records for efficiency.
  • Send the batch to the leader broker.
  • Retry when safe and configured.
  • Report success or failure to the application.

Acknowledgements decide durability risk

The producer's acks setting controls when Kafka confirms a write. Withacks=0, the producer does not wait for confirmation. With acks=1, the leader confirms after writing. With acks=all, the leader waits for the required in-sync replicas. For important business events, acks=all plus appropriate replication settings is the normal direction.

acks settingMeaningRisk profile
acks=0Producer does not wait for broker acknowledgement.Fast but can silently lose records.
acks=1Partition leader acknowledges after local append.Leader failure before replication can lose acknowledged records.
acks=allLeader waits for required in-sync replicas.Stronger durability, usually higher latency.
Producer design rule
Producer settings are business decisions. A clickstream event may tolerate occasional loss. A payment, order, shipment, or audit event usually cannot. Do not copy performance settings before defining the cost of data loss.

Partitioning: how a producer decides where a record goes

Every record a producer sends must be assigned to exactly one partition of the target topic. If the record has a key, the default partitioner hashes that key and maps it deterministically to a partition — the same key always lands on the same partition, for as long as the topic's partition count does not change. If the record has no key, the producer distributes records across partitions using a sticky, batch-aware strategy: it picks one partition and sends a full batch to it, then picks another partition for the next batch, rather than round-robining record by record, which would defeat the purpose of batching by scattering a handful of records across many partially-filled batches.

Keyed vs unkeyed partitioning
# Keyed records: same key -> same partition, every time
send(topic="orders", key="customer-42", value=order1)  -> partition 2
send(topic="orders", key="customer-42", value=order2)  -> partition 2
send(topic="orders", key="customer-91", value=order3)  -> partition 0

# Unkeyed records: sticky batching, not strict round robin
send(topic="orders", key=None, value=event1)  -> batch A, partition 1
send(topic="orders", key=None, value=event2)  -> batch A, partition 1
# batch A fills or lingers out, sent; next batch sticks to a new partition
send(topic="orders", key=None, value=event3)  -> batch B, partition 2

Choosing a key is one of the most consequential decisions a producer makes, because it directly determines ordering guarantees downstream. Keying by customer_id guarantees every event for one customer is processed in order by whichever single consumer owns that partition — exactly the property a per-customer state machine or audit trail needs. Keying by something with very uneven distribution, like a single tenant that accounts for half of all traffic, creates a hot partition that no amount of adding consumers can fix, since a partition is only ever owned by one consumer in a group at a time.

Retries and idempotence at the client level

Producers can retry failed sends automatically, controlled by retries andretry.backoff.ms. Retries are safe against transient errors like a leader election in progress, but naive retries can introduce duplicates or reorder records if multiple requests are in flight at once. Enabling enable.idempotence=true closes this gap: the broker tracks a sequence number per producer session and partition, and silently discards a retried write it has already committed, so retries become safe without any application-level deduplication logic.

SettingWhat it controlsRecommended default
retriesHow many times the producer retries a failed send before giving up.A high value (or Integer.MAX_VALUE) combined with a bounded delivery.timeout.ms.
enable.idempotenceWhether the broker deduplicates retried writes using per-partition sequence numbers.true, for nearly all production producers.
max.in.flight.requests.per.connectionHow many unacknowledged requests can be in flight at once.Safe up to 5 when idempotence is enabled; should be 1 without it, to avoid reordering on retry.

Delivery is asynchronous by default

Most Kafka client libraries send records asynchronously: the call to send a record returns almost immediately, and the actual network write happens on a background thread. This is a performance feature, but it is also the single most common source of silently lost data in beginner producer code. If you never inspect the result of a send — a delivery report, a callback, a returned future — a failed write looks identical to a successful one from the application's point of view.

Ignoring vs handling delivery results
# Silent failure pattern — do not do this
producer.send(topic, key, value)
# no callback, no future check — a broker error here is invisible

# Correct pattern — always attach a delivery callback or check the future
future = producer.send(topic, key, value)
try:
    record_metadata = future.get(timeout=10)
    # confirmed: partition + offset are known and durable per acks setting
except Exception as error:
    log_and_alert("producer delivery failed", error)
    # decide: retry, dead-letter, or fail the request upstream

The distinction matters most at scale: a service sending a few requests per minute might notice a failed send immediately because someone is watching the logs closely. A service sending thousands of records per second with no delivery-result handling can lose a meaningful percentage of its writes during a rough patch of broker instability and never know it happened, because every individual send() call still returned normally — the failure only shows up in the ignored callback or future.

// Part 04 — Batching, linger.ms, and compression

Producers Trade Latency for Throughput on Purpose

A naive producer would send one network request per record. At any real volume this is disastrously inefficient — the fixed cost of a network round trip and a broker append operation dominates the actual work of writing a few hundred bytes. Kafka producers instead group records destined for the same partition into a batch and send the whole batch as one request. Batching is what lets a single producer push millions of records per second without saturating the network with tiny packets.

linger.ms: how long to wait before sending an incomplete batch

A batch fills either when it reaches batch.size bytes or when linger.mselapses since the first record was added to it, whichever comes first. linger.ms is the deliberate trade-off knob: it tells the producer "wait up to this many milliseconds hoping more records show up to fill the batch, even if that means this specific record sits a little longer before being sent." A linger.ms of 0, the default in many clients, sends immediately whenever the network is free — lowest latency per record, but batches end up smaller under light load. A linger.ms of 5-20 lets batches fill up under moderate to heavy load, trading a few milliseconds of added latency for dramatically higher throughput and fewer, larger requests.

linger.ms in practice
# linger.ms = 0 (default in many client libraries)
# record arrives -> sent almost immediately if the network is idle
# under high throughput, natural batching still happens because
# many records arrive faster than one round trip completes

# linger.ms = 10
# record arrives -> producer waits up to 10ms for more records
#   before sending the batch, UNLESS batch.size is reached first
# result under moderate load: fewer, fatter requests to the broker
#   at the cost of up to 10ms of added latency per record
SettingLow value effectHigh value effect
linger.msLower per-record latency, smaller batches, more requests.Higher per-record latency, larger batches, fewer requests, better throughput.
batch.sizeBatches fill and flush quickly, less memory used per partition.More records can accumulate per batch before flushing, amortizing overhead further.

Compression: trading CPU for network and disk

Producers can compress each batch before sending it, using compression.type — common choices are gzip, snappy, lz4, and zstd. Compression happens once, on the whole batch, on the producer side; brokers store the compressed batch as-is and only decompress it when a consumer's client library needs to read individual records back out (the broker itself does not need to decompress to append the batch to the log, which keeps broker CPU usage low). Compressing at the batch level, after records are grouped, gets far better compression ratios than compressing records individually, because similar JSON keys and repeated field names across many records in the same batch compress well together.

CodecCPU costCompression ratioWhen to reach for it
noneZeroNoneVery low volume, or payloads that are already compressed (e.g. images, encrypted blobs).
lz4LowModerateDefault good choice for most high-throughput pipelines — fast compress and decompress.
snappyLowModerateSimilar profile to lz4; older but still common, especially in legacy pipelines.
zstdModerateHighBest ratio for the CPU spent in most benchmarks; a common default on newer clusters with headroom.
gzipHighHighestBest ratio but slowest; used when network or storage cost dominates and CPU is cheap.
Batching and compression compound
A larger linger.ms produces bigger batches. Bigger batches compress better, because compression works on the whole batch as a unit. This is why teams tuning for throughput usually raise linger.ms and enable compression together rather than in isolation — the two settings reinforce each other.

The cost of aggressive batching is not just latency. A larger batch.size and higherlinger.ms mean more unsent data sitting in producer memory (bounded bybuffer.memory) at any moment, which means more data at risk if the producer process crashes before that batch is sent. For latency-sensitive paths — an API request that must return a confirmation to a user — teams often keep linger.ms low or zero and accept the smaller batches. For high-volume background ingestion where nobody is waiting on an individual record, raising linger.ms to 10-50ms is a common and safe throughput win.

A worked example: the request-count math

Concrete numbers make the trade-off easier to reason about than settings alone. Consider a producer sending 200,000 small records per second, each around 200 bytes.

Requests per second at different linger.ms values
Throughput: 200,000 records/sec at ~200 bytes each = ~40 MB/sec

linger.ms = 0, batches average ~20 records (whatever arrives per round trip):
  200,000 / 20 = 10,000 requests/sec sent to the broker

linger.ms = 10, batches average ~2,000 records (filled over 10ms):
  200,000 / 2,000 = 100 requests/sec sent to the broker

Same data volume. 100x fewer requests. Each request now carries
a batch large enough to compress meaningfully well, too --
compounding the throughput gain from Part 04's compression section.

This is why raising linger.ms from 0 to even a small double-digit number is one of the highest-leverage, lowest-risk tuning changes available on a high-volume producer — the added latency per record is small and bounded, while the reduction in broker-side request overhead is often dramatic.

// Part 05 — Consumers

Consumers Pull Records and Own Their Progress

Kafka consumers pull records from brokers. This is different from systems where the broker pushes messages to subscribers. Pull-based consumption lets consumers control their own rate. A fast consumer can fetch more often. A slow consumer can fetch less often. If a consumer is offline, Kafka retains records according to topic retention, and the consumer can catch up later.

A consumer's progress is tracked through offsets. Processing and committing offsets are separate actions. This separation is powerful, but it is also where many reliability bugs begin. If a consumer commits an offset before work is truly complete, a crash can skip work. If a consumer processes work but crashes before committing, the work may happen again after restart.

Safe consumer loop
while running:
  records = consumer.poll()

  for record in records:
    validate(record)
    process_idempotently(record)
    write_result_safely(record)

  commit offsets after successful processing
Commit timingWhat can happen
Commit before processingA crash can lose work because Kafka thinks the record is complete.
Commit after processingA crash can repeat work, so processing should be idempotent.
Auto-commit without thoughtThe client may commit progress unrelated to your actual business success.
Manual commit after durable resultUsually the clearest reliability model for important workflows.

Offsets themselves are stored durably in Kafka — specifically in an internal, compacted topic called __consumer_offsets. A committed offset is just a message: key is (consumer group, topic, partition), value is the offset and some metadata. This is why offset commits are not free — each commit is itself a write to a Kafka topic, replicated like any other write, which is part of why committing after every single record (rather than after a batch) can become a throughput bottleneck under high volume.

Auto-commit: convenient, and dangerous by default

Most client libraries default to enable.auto.commit=true, which periodically commits the latest offset returned by poll() on a fixed interval (auto.commit.interval.ms, typically 5 seconds), regardless of whether your application has actually finished doing anything useful with those records yet. This is convenient for prototypes and genuinely fine for workloads where occasional reprocessing or occasional loss is acceptable. It is a reliability trap for anything that writes to a database, calls a payment API, or otherwise has a real side effect, because the commit clock runs independently of your processing logic.

Why auto-commit can silently skip work
enable.auto.commit = true
auto.commit.interval.ms = 5000

t=0s    poll() returns records offset 100-150
t=1s    application starts processing record 100
t=3s    auto-commit fires -- commits offset 150 (already returned by poll)
t=4s    application crashes while still processing record 112
t=restart  consumer resumes from committed offset 150
           records 112-150 are never processed -- silently skipped

The fix is not necessarily to abandon auto-commit everywhere — it is to understand precisely what it commits and when, and to switch to manual, synchronous commits (enable.auto.commit=falseplus an explicit commitSync() or equivalent after your own processing has durably succeeded) for any workflow where a skipped or duplicated record has a real cost.

Sync vs async manual commits

When committing manually, most clients offer both a synchronous and an asynchronous variant.commitSync() blocks until the broker confirms the commit, retrying on retriable errors, and only returns once the offset is durably recorded — the safest choice, at the cost of adding that round-trip latency to your processing loop. commitAsync() fires the commit without blocking and reports success or failure later through a callback, which keeps the loop moving faster but means a failed commit can be missed if the callback is not checked carefully. A common, pragmatic pattern is to use commitAsync() for routine commits during normal processing, and a final commitSync() right before a graceful shutdown, so the last commit is guaranteed to land even if earlier async commits are still in flight.

A common sync/async commit pattern
try:
    while running:
        records = consumer.poll(timeout=1.0)
        for record in records:
            process_idempotently(record)
        if records:
            consumer.commit_async()   # fast, non-blocking, routine path
finally:
    consumer.commit_sync()            # guaranteed final commit on shutdown
    consumer.close()
// Part 06 — The poll loop

The Poll Loop Has Its Own Timing Rules

Calling consumer.poll() does more than fetch records. It is also how the consumer tells the broker "I am still alive and working." Kafka consumer groups use this fact to detect dead or stuck consumers and rebalance partitions away from them. Understanding the poll loop's timing settings is what separates a consumer that runs for months without a rebalance storm from one that gets kicked out of its group every few minutes under load.

max.poll.records: how much work one poll hands you

max.poll.records caps how many records a single call to poll() returns. It does not cap how many records exist to be read — it only limits the batch size handed to your application per call. This matters because the consumer must call poll() again withinmax.poll.interval.ms or it is considered dead and removed from the group. If your processing logic is slow per record and max.poll.records is set high, you can end up processing for longer than max.poll.interval.ms allows, triggering a rebalance in the middle of your own processing.

max.poll.records and max.poll.interval.ms interacting badly
max.poll.records = 500
max.poll.interval.ms = 300000  (5 minutes, the default)
average processing time per record = 700ms

worst case time to process one poll's batch:
  500 records * 700ms = 350,000ms = ~5.8 minutes

5.8 minutes > 5 minute max.poll.interval.ms
-> consumer is presumed dead, group rebalances mid-batch
-> in-flight work may be duplicated by whichever consumer
   picks up the reassigned partition
Fix the ratio, not just one setting
If per-record processing is slow (calling a slow downstream API, writing to a database per record), either lower max.poll.records so each batch fits comfortably insidemax.poll.interval.ms, or raise max.poll.interval.ms to match realistic processing time, or move slow work off the poll thread entirely (hand records to a worker pool and keep calling poll() promptly). Changing only one side of this ratio without checking the other is how "random" rebalances get introduced into a stable pipeline.

session.timeout.ms and heartbeats: detecting a dead consumer

Separately from the poll call itself, most Kafka client libraries run a background heartbeat thread that pings the group coordinator broker every heartbeat.interval.ms. If the coordinator does not hear a heartbeat within session.timeout.ms, it assumes the consumer has crashed and triggers a rebalance, even if the consumer's main thread is still technically alive but stuck. This is a separate failure mode from the poll-interval timeout above:session.timeout.ms catches a genuinely frozen or network-partitioned process, while max.poll.interval.ms catches a process that is alive and heartbeating but stuck doing slow application work between polls.

SettingWhat it detectsTypical starting value
session.timeout.msThe consumer process is unresponsive or network-partitioned from the coordinator.10-45 seconds, depending on client version defaults.
heartbeat.interval.msHow often the background thread pings the coordinator — usually about a third of session.timeout.ms.3 seconds, scaled with session.timeout.ms.
max.poll.interval.msThe application is alive but has not returned to call poll() again in time — usually because per-batch processing took too long.5 minutes by default, tune to match real processing time.

The practical takeaway: if you see unexplained rebalances, check which timeout tripped. A trippedsession.timeout.ms usually points to network issues, GC pauses, or resource starvation on the consumer host. A tripped max.poll.interval.ms almost always points to slow business logic inside the poll loop relative to max.poll.records.

A concrete heartbeat timeline
session.timeout.ms = 10000       (10 seconds)
heartbeat.interval.ms = 3000     (3 seconds)

t=0s    consumer sends heartbeat, coordinator resets its timer
t=3s    consumer sends heartbeat, coordinator resets its timer
t=6s    consumer sends heartbeat, coordinator resets its timer
t=8s    a long GC pause freezes the whole JVM, including the
        background heartbeat thread
t=9s    coordinator has not heard a heartbeat since t=6s -- still
        within the 10s window, no action yet
t=17s   GC pause finally ends, but 11 seconds have now passed
        since t=6s -- past the 10s session.timeout.ms
t=17s   coordinator has already declared this consumer dead and
        triggered a rebalance; another consumer now owns its
        partitions

This is exactly why session timeout tuning and JVM garbage collection tuning are often discussed together for Java-based Kafka consumers — a heartbeat thread that shares a process with a long GC pause is just as unresponsive during that pause as a genuinely crashed process, from the coordinator's point of view.

// Part 07 — Consumer groups

Consumer Groups Let Readers Share Work

A consumer group is a named team of consumers. Kafka assigns partitions to consumers within the group. For any one partition, only one consumer in the same group reads it at a time. This lets a service scale horizontally without every instance duplicating the same work.

Consumer group assignment
orders topic has 4 partitions

billing group:
  billing-consumer-1 -> partition 0, partition 1
  billing-consumer-2 -> partition 2, partition 3

analytics group:
  analytics-consumer-1 -> partition 0, partition 1, partition 2, partition 3

Billing and analytics are separate groups.
They read the same topic independently.

This is why Kafka can behave like a queue and a publish-subscribe system at the same time. Inside one consumer group, partitions are shared, so records are distributed across workers. Across different consumer groups, each group gets its own independent read position, so the same records can feed many applications.

Parallelism limit
If a topic has 4 partitions, one consumer group can actively use at most 4 consumers for that topic. Adding a fifth consumer to the same group does not create a fifth partition. It usually waits idle.

Adding or removing a consumer from a group — including a crash, a deploy, or an autoscale event — triggers a rebalance: the group coordinator reassigns partitions among whichever consumers are currently alive. During a rebalance, consumers involved briefly stop processing while new assignments are handed out. Newer cooperative rebalancing strategies reduce this pause by only reassigning the partitions that actually need to move, instead of revoking every partition from every consumer and reassigning from scratch, which is what the older eager rebalancing protocol does.

Eager vs cooperative rebalancing

Under the original, eager rebalancing protocol, every consumer in the group revokes all of its assigned partitions the moment a rebalance starts, even partitions that will be reassigned right back to the same consumer, and the group only resumes processing once every member has a full new assignment. This is simple to reason about but means the entire group pauses on every membership change, however small. The cooperative (incremental) rebalancing protocol, the default in current client versions, instead computes the minimal set of partitions that actually need to move and only revokes those, letting every other consumer keep processing its unaffected partitions throughout the rebalance.

ProtocolWhat happens on a rebalancePause during rebalance
EagerEvery consumer revokes every partition; the whole group waits for a fresh assignment.Whole group pauses, even consumers whose assignment does not actually change.
Cooperative (incremental)Only the specific partitions that need to move are revoked and reassigned.Unaffected consumers keep processing throughout; only the moving partitions pause briefly.

Static membership: avoiding unnecessary rebalances entirely

A rolling deploy that briefly stops and restarts each consumer instance can trigger a full rebalance for every single restart, even though the group's real membership is unchanged a few seconds later. Setting group.instance.id to a stable, unique value per consumer instance enables static membership: the group coordinator recognizes a consumer reconnecting with the same instance ID within session.timeout.ms as the same member returning, rather than as a departure followed by a new arrival, and skips the rebalance entirely. This is a meaningful operational win for any consumer group that gets redeployed frequently.

// Part 08 — Pull vs push

Why Kafka Chose Pull Over Push

Many older messaging systems push messages to subscribers: the broker decides when to send data, and the consumer's job is to keep up. Kafka deliberately inverted this. The consumer decides when to ask for more data by calling poll(). This single design choice explains a large share of Kafka's operational behavior, so it is worth understanding in depth rather than as a trivia fact.

The problem push-based systems run into

In a push model, the broker must decide a send rate for each consumer without knowing that consumer's true current capacity. If the broker guesses too high, it floods a slow consumer faster than it can process, forcing the consumer to either drop messages, buffer them until it runs out of memory, or apply some kind of flow-control signal back to the broker asking it to slow down — which is effectively reinventing a pull model through a side channel. If the broker guesses too low, fast consumers sit idle waiting for data that could have already been sent.

ModelWho controls rateFailure mode under a slow consumer
PushThe broker decides how fast to send.Consumer is overwhelmed unless the broker implements its own flow control back-channel — added complexity to solve a problem pull avoids by construction.
PullThe consumer decides how often to call poll() and how much to request.Consumer naturally falls behind (visible as growing lag) instead of being overwhelmed; it resumes at its own pace whenever it is ready.

With pull, backpressure is implicit and safe by default: a slow consumer simply callspoll() less often or processes what it receives more slowly, and the unread records just sit durably in the broker's log until the consumer is ready for them, bounded only by the topic's retention period. Nothing needs to be buffered in the consumer's memory beyond what it asked for. Nothing needs to be dropped. The broker does not need to track each consumer's real-time capacity — it only needs to serve whatever range of offsets each consumer's next fetch request asks for.

Pull also enables batching on the consumer's own terms

Because the consumer initiates each fetch, it can ask for as many records as it can comfortably handle in one round trip (bounded by max.poll.records and related fetch-size settings covered in Part 06), rather than receiving records one at a time as a broker decides to push them. This is part of why a single Kafka consumer can sustain very high throughput — each network round trip can carry a large batch of records the consumer itself decided it wanted.

The trade-off pull makes
Pull is not free. It means slightly higher latency for a truly idle topic, since a consumer polling on an interval might wait up to that interval before noticing new data (Kafka mitigates this with long-poll style fetch requests that return promptly once data arrives, rather than requiring tight polling loops). In exchange, Kafka gets a backpressure model that cannot overwhelm a consumer by construction — a property that matters far more at production scale than shaving milliseconds off an idle topic's latency.

What "falling behind safely" looks like in numbers

If a producer sustains 50,000 records/second and a consumer can only sustain 40,000 records/second, a push-based system without its own flow control would either drop the excess 10,000 records/second or force the consumer to buffer them in memory until it runs out. A pull-based consumer instead simply falls 10,000 records/second further behind in the broker's durable log every second — fully visible as growing consumer lag, and fully recoverable later, bounded only by the topic's retention window, exactly as covered in this module's monitoring guidance in Part 10.

// Part 09 — Metadata, leadership, and cluster coordination

Clients Must Know Which Broker Leads Which Partition

Kafka clients are metadata-aware. This is different from a simple load-balanced HTTP service where any backend can handle any request. In Kafka, writes and reads for a partition go through the current partition leader. If leadership changes because a broker fails or maintenance occurs, clients refresh metadata and send future requests to the new leader.

This explains common errors like NotLeaderOrFollower or temporary timeouts during broker restarts. The client may have old metadata for a short period. Good clients refresh and retry. Good applications still log and monitor these errors so real cluster instability is not ignored.

EventWhat Kafka doesWhat clients do
Broker startsIt joins the cluster and receives partition assignments.Clients may discover it through metadata.
Leader failsThe controller elects a new leader from suitable in-sync replicas.Clients refresh metadata and retry requests.
Partition reassignedReplicas move between brokers.Clients update where they send reads/writes.
Topic createdMetadata changes.Clients discover partitions and leaders.

How brokers actually coordinate as a cluster

The controller broker described in Part 02 is what makes leadership changes cluster-wide and consistent rather than something each broker figures out independently. In KRaft mode, cluster metadata — which topics exist, how many partitions each has, who currently leads each partition, which brokers are in each partition's ISR — is itself stored as a replicated, ordered log, managed by the Raft protocol among the controller nodes. Every broker in the cluster maintains a local, continuously updated copy of this metadata log, which is what lets any broker answer a client's metadata request correctly and quickly, without having to ask another broker first.

Leadership change, end to end
1. broker-3 (leader for orders partition 0) crashes
2. controller quorum detects the missed heartbeat
3. controller checks orders-partition-0's ISR: [broker-1, broker-2]
4. controller elects broker-1 as the new leader
5. controller writes this change to the cluster metadata log
6. every broker replicates the updated metadata log
7. producer's next write to orders partition 0 is rejected by the
   old cached leader info (NotLeaderOrFollower) or times out
8. producer refreshes metadata, learns broker-1 is now the leader
9. producer resumes sending to broker-1 -- no data was lost because
   acks=all had already required broker-2's replica to be caught up

This is also why acks=all and a correctly sized in-sync replica set matter beyond just the producer-side durability story from Part 03 — they are what guarantee that whichever replica the controller picks as the new leader after a failure is actually caught up, so a leadership change does not silently roll back recently acknowledged writes.

What clients actually cache, and when they refresh it

A client does not fetch fresh metadata before every single request — that would defeat the purpose of knowing partition leaders in the first place. Instead, it caches the metadata it receives and only refreshes it on specific triggers: periodically on a fixed interval (metadata.max.age.ms), immediately when a request to a cached leader fails with an error indicating that broker is no longer the leader, and on startup or when a new topic is referenced that the client has no cached metadata for at all.

TriggerWhy the client refreshes
metadata.max.age.ms elapsesRoutine background refresh, even if nothing has failed, to catch cluster changes proactively.
NotLeaderOrFollowerException on a requestThe cached leader for this partition is stale — the broker just told the client so directly.
Unknown topic or partition referencedThe client has never seen this topic before and has no cached entry to use at all.
Connection to the cached leader fails outrightThe broker may be down or unreachable; the client needs a fresh view of who else could serve this partition.

This caching-with-triggered-refresh design is why a brief broker restart is usually invisible to a well-behaved client beyond a short burst of retried requests — the client does not need to be told proactively that something changed; it discovers this the moment it tries to use stale information and gets corrected.

// Part 10 — Putting it together

Monitoring the Whole Picture: Producers, Brokers, Consumers

Every setting covered in this module maps to a real, observable metric. Treating producers, brokers, and consumers as three independently healthy systems misses the point — a Kafka pipeline is only as reliable as the weakest link across all three, and the right monitoring makes that weak link visible before it becomes an incident rather than after.

RoleMetric to watchWhat it tells you
Producerrecord-error-rate / delivery failuresWhether sends are actually succeeding, since sends are asynchronous by default and failures are invisible unless checked, per Part 03.
Producerrequest-latency-avg and batch-size-avgWhether linger.ms and compression settings from Part 04 are actually producing the batching behavior intended.
Brokerunder-replicated-partitionsWhether any partition currently has fewer in-sync replicas than its full replica set — a direct durability risk signal.
Brokeractive-controller-countShould be exactly 1 across the whole cluster at all times; 0 or more than 1 indicates a serious coordination problem, per Part 02 and Part 09.
Consumerrecords-lag-max (per partition)The single most important consumer health signal — a growing value means the consumer cannot keep up, per Part 05 and Part 06.
Consumerrebalance rateFrequent rebalances point to either session.timeout.ms or max.poll.interval.ms being tripped repeatedly, per Part 06 and Part 07.
A pipeline is only as reliable as its weakest configured piece
A producer with acks=all writing to a broker with min.insync.replicas=2still loses the durability story if the consumer reading that data commits offsets before processing finishes. Every layer's guarantees are necessary but none is sufficient on its own — this is why this module covers all three roles together instead of in isolation.
// Misconceptions

Five Misconceptions About Kafka Clients

✕ ""bootstrap.servers has to list every broker in the cluster""
Part 02 covers this directly: bootstrap.servers only needs enough reachable brokers to let the client discover cluster metadata. Listing two or three is normal production practice, purely so one being down does not block startup.
✕ ""Kafka pushes messages to consumers like a webhook""
Part 08 explains the opposite is true by design: consumers pull by calling poll(), which is exactly what lets a slow consumer fall behind safely instead of being overwhelmed by a broker guessing at its capacity.
✕ ""acks=all means the message can never be lost, no matter what""
Part 03 and Part 09 together show the real contract: acks=all waits for the in-sync replica set at that moment. If min.insync.replicas is not also configured, a shrunk ISR can still satisfy acks=all with weaker guarantees than the name implies.
✕ ""Adding more consumers to a group always increases throughput""
Part 07 is explicit that a partition is owned by exactly one consumer in a group at a time. Once consumers equal partitions, additional consumers in that same group sit idle — you need more partitions, not more consumers, to go further.
✕ ""A rebalance always means a consumer crashed""
Part 06 separates two distinct timeout mechanisms: session.timeout.ms catches a genuinely dead or network-partitioned process, while max.poll.interval.ms catches a perfectly healthy process that simply took too long processing a batch before calling poll() again. Diagnosing which one tripped points you to a completely different fix.
// What This Looks Like at Work

What This Looks Like on Day One

At Robinhood: a new trade-confirmation consumer keeps getting kicked out of its consumer group every few minutes under market-open load, even though the process never crashes. You pull the consumer's logs and see repeated rebalance events with no matching error. Following Part 06, you check max.poll.records against actual per-record processing time and find each poll's batch is taking almost 6 minutes to process against a 5-minutemax.poll.interval.ms. Lowering max.poll.records from 500 to 100 fixes it without touching business logic.

At DoorDash: the platform team is designing a new order-events producer expected to handle dinner-rush peak load. Using Part 04, they set linger.ms=15 andcompression.type=lz4 for the high-volume delivery-tracking topic, since no single record needs to be visible within milliseconds, while keeping linger.ms=0 on the separate payment-authorization topic where a customer is actively waiting on a checkout response. Same producer library, two different latency-versus-throughput trade-offs made deliberately per topic.

In a system design interview: "Why does Kafka use a pull model instead of push?" The strong answer, straight from Part 08, is not just "consumers ask for data" — it is that pull makes backpressure implicit and safe by construction: a slow consumer simply falls behind in the durable log instead of being overwhelmed, and the broker never needs to track each consumer's real-time capacity to avoid flooding it.

// Interview Prep

5 Interview Questions — With Complete Answers

Q1. Walk me through what happens, end to end, when a producer sends one record to a topic it has never written to before.
The producer first needs metadata: it doesn't yet know which broker leads which partition for this topic, so per Part 02 it uses whatever brokers are listed in bootstrap.servers to fetch that metadata. If the topic exists, the response includes the partition list and current leaders. The producer then, per Part 03, serializes the key and value, decides which partition the record belongs to (using the key's hash if one is provided, or a partitioning strategy if not), and adds the record to an in-memory batch for that specific partition, per Part 04. That batch is held until it fills to batch.size or linger.ms elapses, whichever comes first, then sent as one request to the partition's current leader broker. The leader appends the record to its local log and, depending on the acks setting from Part 03, either responds immediately, waits for its own local write, or waits for the configured in-sync replicas to confirm before acknowledging back to the producer.
Q2. What is the practical difference between session.timeout.ms and max.poll.interval.ms, and why do both exist?
They detect two different failure modes, both covered in Part 06. session.timeout.ms is enforced through a background heartbeat thread most client libraries run independently of your application code — it detects a consumer that is truly dead or unreachable, such as a crashed process or a severed network connection, because heartbeats simply stop arriving. max.poll.interval.ms is enforced against the main thread's call pattern to poll() itself. A consumer can be heartbeating perfectly fine on its background thread while its main thread is stuck doing slow application work — a slow downstream API call inside the processing loop, for example — and never returning to call poll() again. Without a separate timeout for that, the group coordinator would have no way to detect a consumer that is alive but effectively stuck. In practice, if I see a rebalance, the first thing I check is which timeout tripped — that tells me whether to look at infrastructure health (session.timeout.ms) or at per-batch processing time versus max.poll.records (max.poll.interval.ms).
Q3. Why does Kafka use a pull-based consumer model instead of pushing messages to consumers?
Part 08 is the reference here. In a push model the broker has to guess each consumer's real-time processing capacity in order to decide a safe send rate. Guess too high and a slow consumer gets flooded, forcing either dropped messages, unbounded buffering, or a flow-control channel back to the broker — which is really just reinventing pull through the back door. Guess too low and fast consumers sit idle. With pull, the consumer itself decides when to call poll() and effectively how much work to take on. A slow consumer just falls behind — visible directly as growing consumer lag — while the unread records sit safely and durably in the broker's log until the consumer is ready, bounded only by the topic's retention window. The broker never needs to track per-consumer capacity to avoid overwhelming anyone. The trade-off is slightly higher latency for an idle topic, which Kafka mitigates with fetch requests that return promptly once new data is available rather than requiring tight, wasteful polling.
Q4. A team wants to increase producer throughput on a high-volume topic without adding brokers. What levers would you pull?
Starting from Part 04, I'd first look at batching: is linger.ms set to 0? If nothing is waiting on individual record latency for this specific topic, raising linger.ms to somewhere in the 10-50ms range lets batches fill up more before being sent, meaning fewer, larger requests to the broker for the same volume of data. Second, I'd check compression.type. Enabling lz4 or zstd compresses whole batches before they go over the network, which reduces both network bytes and the disk footprint on the broker, at a modest CPU cost on the producer side — usually a very good trade for high-volume topics. Third, I'd check whether the producer is actually using async sends properly rather than blocking on every future, per Part 03 — a producer that waits for each record's delivery confirmation before sending the next defeats the purpose of batching entirely. I would not reach for more partitions or more brokers until these client-side levers were already tuned, since they're free relative to infrastructure changes.
Q5. What is the role of the controller broker, and how does KRaft mode change how it is elected?
Per Part 02 and Part 09, the controller is the one broker in a cluster responsible for cluster-wide coordination decisions — most importantly, electing a new partition leader from the in-sync replica set whenever a current leader fails, and propagating that change to the rest of the cluster's metadata. Historically Kafka used ZooKeeper, an external coordination service, to elect the controller and to store this metadata. In KRaft mode, which is the current default and what any new cluster should run, a quorum of dedicated controller nodes replicates cluster metadata among themselves directly using the Raft consensus protocol, with no external dependency. Every broker keeps a locally replicated copy of that metadata log, which is what lets any broker answer a client's metadata query quickly and correctly. The end-to-end effect for a client is the same either way — producers and consumers refresh metadata and retry against whichever broker is now the leader — but KRaft removes an entire external system to operate and generally fails over faster.
// Common Mistakes

The Mistakes That Make Kafka Clients Unreliable

Thinking bootstrap.servers must include every broker
Part 02 covers why this is unnecessary — a client only needs a reachable subset to bootstrap metadata discovery, and it learns the full picture of partition leaders from there.
Thinking Kafka pushes records to consumers instead of consumers polling
Part 08 walks through why this distinction matters operationally, not just semantically — it is the entire reason Kafka backpressure is safe by construction instead of requiring a flow-control side channel.
Using auto-commit for workflows where database writes or API calls must finish first
Part 05's commit-timing table is the reference: auto-commit fires on its own schedule, unrelated to whether your actual business-critical work has durably succeeded, which can silently skip work on a crash.
Setting max.poll.records high without checking it against real per-record processing time
Part 06 shows exactly how this produces mysterious, hard-to-reproduce rebalances — the batch simply takes longer to process than max.poll.interval.ms allows, and the group coordinator has no way to know the consumer is actually still working.
Adding more consumers than partitions and expecting more throughput
Part 07 is explicit that partition count is a hard ceiling on active parallelism within one consumer group — extra consumers beyond that ceiling sit idle rather than doing work.
// Error Library

Errors You Will Hit — And Exactly Why They Happen

Producer logs repeated "NotLeaderOrFollowerException" for a partition it was writing to successfully seconds earlier
Cause: The broker that was leading this partition failed, was restarted, or lost leadership during a planned rebalance of partition assignments. The producer's cached metadata still points at the old leader, and the broker it is talking to correctly rejects the write because it no longer leads that partition.
Fix: This is expected transient behavior, not a bug, as long as it is brief — the client library should automatically refresh metadata and retry against the new leader, per Part 09. If it persists for more than a few seconds, check the controller's logs for repeated leader elections, which usually points to broker instability rather than a one-off failover.
Consumer group rebalances every few minutes with no crash or deploy correlated to it
Cause: As covered in Part 06, this is almost always max.poll.interval.ms being exceeded — the consumer's main thread is taking longer to process one poll()'s worth of records than the interval allows, even though its heartbeat thread is reporting it as alive.
Fix: Compare max.poll.records times average per-record processing time against max.poll.interval.ms. Lower max.poll.records, raise max.poll.interval.ms to match realistic processing time, or move slow per-record work off the poll thread entirely.
A downstream system receives the same order confirmation event twice, seconds apart
Cause: Per Part 05's commit-timing table, the consumer processed the record, performed its side effect (sending the confirmation), but crashed or was rebalanced away before committing the offset. On resumption, either the same consumer or a new one re-reads the same offset and processes it again.
Fix: Make the downstream side effect idempotent — for example, keyed on a unique order confirmation ID that is checked before sending — rather than trying to eliminate all possible duplicate delivery, which Kafka's at-least-once default does not guarantee against on its own.
Producer throughput plateaus far below expected, with high CPU usage on the producer host
Cause: Compression codec mismatch with available CPU headroom, most commonly gzip chosen for its compression ratio without checking its CPU cost, per Part 04's codec comparison table — or linger.ms left at 0 under high volume, causing far more, smaller network requests than necessary.
Fix: Switch to lz4 or zstd for a better throughput-per-CPU trade-off, and raise linger.ms to let batches fill up more before sending, if per-record latency is not critical for this specific topic.
A single partition's consumer lag climbs steadily while every other partition in the same group sits near zero
Cause: One consumer in the group owns that partition alone — per Part 07, a partition is never shared between consumers in the same group — and it is structurally slower than the volume arriving on that specific partition, often because of a hot key concentrating too much traffic there.
Fix: Adding more consumers to the group will not help once you are already at one consumer per partition; the fix is either a better partition key that spreads the hot entity's traffic more evenly, or splitting that specific workload out to more partitions.

🎯 Key Takeaways

  • Producers write records, consumers read records, and brokers store and serve records — but every one of those verbs hides real mechanics: metadata discovery, batching, acknowledgement, offset commit timing, and cluster-wide leadership coordination.
  • Kafka clients are metadata-aware and always talk to the current partition leader; the controller broker, elected via KRaft in current Kafka versions, is what makes leadership changes consistent across the whole cluster.
  • Producer batching (linger.ms, batch.size) and compression (lz4, zstd, gzip) trade a small amount of added latency for significantly higher throughput, and the two settings reinforce each other.
  • The consumer poll loop is governed by two independent timeout mechanisms — session.timeout.ms for a truly dead consumer, and max.poll.interval.ms for a live consumer stuck processing too large a batch — and diagnosing rebalances means figuring out which one tripped.
  • Kafka's pull model makes backpressure implicit and safe: a slow consumer falls behind in a durable log instead of being overwhelmed, which is a deliberate trade against the small added latency of an idle topic.
  • Consumer groups distribute partition work, but partition count is a hard ceiling on active parallelism — more consumers than partitions in the same group just sit idle.
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...