Fetch Protocol & Fetch Sessions in Kafka
How every read in Kafka really works, from a consumer asking for its next messages to a follower copying a leader, and how fetch sessions stop the cluster from repeating itself thousands of times a second.
Every message you have ever read from Kafka arrived because of one small request: Fetch. A consumer in a shopping app fetches order events. A follower broker fetches the same events so that it can keep a spare copy. A dashboard fetches metrics. Even the servers that manage the cluster’s own records use the same idea. Fetch is the workhorse of Kafka, and on a busy cluster it runs many thousands of times every second.
That is why two questions matter so much. First, what exactly is inside a fetch request and a fetch response, and why does Kafka make the broker wait before answering? Second, what happens when a single client keeps asking about thousands of partitions, most of which have nothing new? The second question led to a clever feature called fetch sessions. This guide starts from the very basics and builds up, step by step, until you can explain both ideas in a design review, tune them on a real cluster, and read the metrics that tell you when something is wrong.
1What Is the Fetch Protocol?
The plain-English idea first. The details come one layer at a time.
Kafka brokers store data in logs, and clients read from those logs. The way a client reads is not by having the broker push messages out. Instead, the client sends a request that says, “Here is the partition I care about and here is the offset where I stopped. Please give me what comes next.” The broker answers with the next chunk of data. This request and its answer are called Fetch. Because the reader asks and the broker responds, we call Kafka a pull-based system.
The Fetch protocol is the exact set of rules for how that conversation happens. It says which fields a request must carry, which fields a response contains, how long the broker may wait before replying, how much data may be sent, and what errors can come back. A fetch session is a shortcut layered on top of those rules. It lets a client and a broker remember what they talked about last time, so that the next request only needs to mention what changed.
Think of a newspaper delivery habit. Without any shortcuts, every morning you call the shop and say, “I read up to page 12 of the city paper, page 40 of the sports paper, page 7 of the food paper…” and you list every paper you follow. That is a full fetch request. With a fetch session, you tell the shop once, “Same list as yesterday.” From then on you only call to say, “The sports paper moved to page 41,” and the shop only replies about papers that actually have new pages.
Why this topic deserves your time
Fetch sits under almost every performance and reliability story in Kafka. Slow consumers, lagging followers, shrinking ISRs, high network bills, sudden CPU spikes on brokers after a restart, and strange delays of exactly half a second are all connected to how fetch behaves. Engineers who understand fetch can explain these symptoms in minutes. Engineers who do not often chase them for days.
Fetch is how data leaves a broker, and fetch sessions are how Kafka avoids describing the same long list of partitions over and over again.
2Kafka Basics You Need First
A short tour of the parts that fetch talks about. Skim it if you already know Kafka well.
Kafka is a system for moving streams of events. Programs that write events are producers, and programs that read them are consumers. The servers in the middle are brokers, and a group of brokers is a cluster. Events are organized into topics, and each topic is cut into partitions. A partition is the unit that fetch works on: every fetch request asks about one or more specific partitions.
Log
Each partition is stored as an append-only log made of segment files. New records are added at the end and old ones are never edited.
Offset
The number that identifies each record inside a partition: 0, 1, 2 and so on. A fetch request names the offset it wants next.
Leader
For every partition, one broker is the leader. Clients send fetch requests to it, and it is the only broker that accepts writes for that partition.
Follower
Other brokers hold spare copies of the partition. They keep up by sending fetch requests to the leader, just like consumers do.
High watermark
The offset up to which all in-sync copies hold the data. Normal consumers may only read records below this line.
ISR
The in-sync replicas: the leader plus the followers that are keeping up. The leader uses follower fetches to decide who belongs.
Consumer group
A set of consumers that share the partitions of a topic. Each member sends its own fetch requests for the partitions it owns.
Controller
The part of the cluster that decides which broker leads which partition, and that tells everyone about changes.
Records travel in batches
Kafka does not store or send records one at a time. Producers group records into batches, often compressed, and brokers store each batch exactly as it arrived. When a consumer fetches, the broker sends whole batches. This one design choice makes Kafka fast, because the broker can send bytes straight from disk without unpacking them. It also explains a few surprising details you will meet later, such as why a fetch response can start slightly before the offset you asked for.
flowchart LR
subgraph CL["Kafka Cluster"]
L["Broker 1
Leader of Partition 0"]
F1["Broker 2
Follower of Partition 0"]
F2["Broker 3
Follower of Partition 0"]
end
P["Producer App"] -->|"Produce"| L
F1 -->|"Fetch"| L
F2 -->|"Fetch"| L
C1["Consumer A"] -->|"Fetch"| L
C2["Consumer B"] -->|"Fetch"| L
LB["Cluster Metadata
tells clients who leads"] -.-> C1
LB -.-> C2
LB -.-> P
Notice in Figure 1 that followers and consumers look almost the same from the leader’s point of view. Both send fetch requests, and both receive batches of records. The only difference is who they are and what the leader does with the information. That shared design keeps Kafka simple: one mechanism serves both readers and replicas.
3Who Sends Fetch Requests?
Two very different kinds of clients use the same request, and the broker treats them differently.
The first kind of client is the ordinary consumer. It fetches records so that an application can process them. The second kind is the follower replica, which is a broker copying a partition from its leader. In the request there is a field that says who is asking. Consumers put the value -1 in that field. Brokers put their own broker ID there. The leader looks at this value and knows whether it is serving a reader or a replica.
| Question | Consumer fetch | Follower fetch |
|---|---|---|
| Who sends it? | An application using a Kafka client | Another broker in the same cluster |
| What is it for? | Reading records to process them | Copying records to keep a replica |
| How far may it read? | Up to the high watermark, or the last stable offset in transactional mode | Up to the end of the leader’s log |
| What does the leader learn? | Nothing that changes the partition | How far the follower has copied, which moves the high watermark and the ISR |
| Who may send it? | Users with read permission on the topic | Brokers with cluster-level permission |
| How often? | As fast as the application consumes | Constantly, as long as the follower is running |
Consumers, followers and the cluster’s own logs
There is a third user of fetch that many people miss. In modern clusters that run without ZooKeeper, the controllers keep the cluster’s metadata in their own replicated log, and they copy it between themselves using the same pull-based fetch idea. The mechanics are slightly extended for that purpose, but the shape is the same: a follower keeps asking the leader, “What is new after this offset?” This shows how central the pattern is. Kafka uses one simple idea for user data and for its own bookkeeping.
Picture a library with one master copy of a growing logbook. Some visitors come to read the newest pages. Some assistants come to copy the newest pages into their own logbooks. Both walk up to the same librarian and ask, “What was added after page 500?” The librarian gives them the same kind of answer, but only listens to the assistants’ reports when deciding which copies can be trusted.
With pull, each reader controls its own speed. A slow consumer simply asks less often and never floods itself. The broker does not need to track which readers are ready, and it does not hold half-sent data for slow clients. The cost is that a reader with nothing new to read could ask again and again with empty answers. Long polling, described in a later chapter, is Kafka’s answer to that cost.
4Anatomy of a Fetch Request
Let us open the envelope and look at every important field. Field names vary a little between versions, so read this as the shape, not as a word-for-word specification.
Kafka clients and brokers speak a binary protocol over TCP. Every message on the wire starts with a small header that says which kind of request it is (for us, Fetch), which version of that request it uses, a number that lets the client match answers to questions, and the client’s name. Clients and brokers first agree on the highest version they both understand, so that new brokers can still talk to old clients and the other way around. After the header comes the body. For Fetch, the body has two layers: fields that apply to the whole request, and fields that apply to each partition.
Fields that apply to the whole request
| Field | Plain meaning |
|---|---|
| Replica ID | Who is asking: -1 for a consumer, or a broker’s ID for a follower |
| Max wait | The longest the broker may hold the request if it has no data yet |
| Min bytes | How much data must be ready before the broker answers early |
| Max bytes | The soft upper limit on the size of the whole response |
| Isolation level | Whether to see only committed transactions or everything below the high watermark |
| Session ID and session epoch | Which fetch session this request belongs to, and its step number in that session |
| Rack ID | Where the client lives, so the broker can suggest a nearby replica |
| Forgotten topics | Partitions the client wants to remove from its session |
Fields that apply to each partition
| Field | Plain meaning |
|---|---|
| Topic and partition | Which log to read. Newer versions identify topics by a unique ID as well as by name |
| Fetch offset | The offset of the next record the client wants |
| Partition max bytes | How much data this one partition may contribute to the response |
| Current leader epoch | The leader era the client believes is current, used to catch stale views |
| Last fetched epoch | The leader era of the last record the client received, used to detect diverging logs |
| Log start offset | Sent by followers so the leader knows how far back the follower’s log begins |
“I am broker 2. I will wait up to half a second. I want at least one byte, at most fifty megabytes in total. For partition 7 of the orders topic, I stopped at offset 90,210, I believe the leader era is 4, and please send up to one megabyte.” Once you can read a request like that, log lines and network captures stop looking mysterious.
The key idea is that the request is per broker, not per partition. A consumer that owns twelve partitions on the same broker sends one request that lists all twelve. This batching saves network round trips, but it also creates the problem that fetch sessions later solve: the list of partitions can be very long, and most entries repeat unchanged.
5Anatomy of a Fetch Response
What the broker sends back, and the small details in it that keep the whole cluster honest.
The response mirrors the request. It carries whole-response fields, and then a list with one entry for each partition that has something to say. The most obvious content is the records themselves, delivered as batches. But the response also carries several other facts that clients and followers depend on.
| Field | Plain meaning | Who uses it |
|---|---|---|
| Throttle time | How long the client was slowed by a quota | Clients report it as a metric |
| Error code | Whether the request or a partition had a problem | Everyone |
| Session ID | The session the broker created or is continuing | Clients that use sessions |
| High watermark | The leader’s current committed line for this partition | Followers copy it, consumers use it for lag |
| Last stable offset | The point below which all transactions are decided | Transactional consumers |
| Log start offset | The oldest offset still kept by the leader | Followers and consumers |
| Aborted transactions | A list of cancelled transaction ranges | Consumers in read-committed mode |
| Diverging epoch | A hint that the client’s log has split from the leader’s | Followers and modern consumers |
| Preferred read replica | A nearby replica that could serve this client | Rack-aware consumers |
| Records | The batches of data | Everyone |
A response that starts too early
Because the broker sends whole batches, the first batch in a response can begin a little before the offset the client asked for. Imagine a batch that holds offsets 100 to 119, and the client asks for offset 105. The broker cannot cut the batch in half without unpacking it, so it sends the whole batch, and the client quietly ignores records 100 to 104. This is normal and costs very little, and it is a good example of Kafka trading a tiny bit of wasted bandwidth for a large saving in broker work.
A response can be empty and still useful
Sometimes a response carries no records at all. This is not a mistake. A follower that is fully caught up still needs to hear the leader’s latest high watermark, and the leader still needs to hear that the follower is alive and keeping up. An empty response is how a quiet partition keeps the relationship going. It is also how a broker tells a client, “Nothing new, but here is an error, a leader change or a new suggested replica.”
A courier delivering parcels sometimes arrives with no parcel, only a note: “Nothing today, but your building’s entrance has moved to the back door.” The trip was still worthwhile, because the note changes what you do next. Empty fetch responses carry notes like that all the time.
6Long Polling: Why Fetch Waits
The broker deliberately sits on your request. That pause is a feature, not a delay.
Imagine a consumer that reads a quiet topic. If the broker answered instantly with “nothing yet”, the consumer would immediately ask again, and again, hundreds of times a second, wasting network, CPU and battery. To avoid this, Kafka uses long polling. When a fetch request arrives and there is not enough data to fill it, the broker does not reply. It parks the request in a waiting area and answers only when one of two things happens: enough data has arrived, or the maximum wait time has run out.
Two request fields control this. Min bytes says how much data must be available before an early answer is worth sending. The default is one byte, which means “answer as soon as there is anything at all”. Max wait says the longest time the broker may hold the request. On consumers the default is 500 milliseconds. The waiting area is called the purgatory in Kafka’s code, and the parked request is a delayed fetch.
sequenceDiagram autonumber participant C as Consumer participant B as Broker Leader participant PG as Purgatory C->>B: Fetch offset 500, min 1 byte, max wait 500 ms B->>B: Look for data at offset 500 and find none B->>PG: Park the request and start the timer Note over PG: Waiting for new data or for the timer B->>B: Producer writes offset 500 B->>PG: New data is now available PG-->>C: Fetch response with the new batch C->>B: Fetch offset 501, min 1 byte, max wait 500 ms
What ends the wait
Enough bytes arrive
New records are written, and the total available data reaches the min bytes limit. The broker answers at once with what it has.
The timer runs out
The max wait time passes with too little data. The broker answers anyway, sometimes with an empty response, so that the client never waits forever.
Something about the partition changes
If the broker stops being the leader, or the partition has an error, the parked request is completed early with an error so that the client can react.
The trade-off behind the two numbers
A higher min bytes and a longer max wait make the broker collect more data per answer. Fewer, bigger responses mean higher throughput and less CPU per byte, but a message may sit and wait until the batch fills up. A tiny min bytes and short wait give faster delivery of each record but more requests. For most applications the defaults are a sensible middle. If you run a heavy analytics job, raising the min bytes can pay off. If you run a live trading feed, you keep it small.
Production example: the half-second mystery
A team notices that one of their small test consumers sees a message exactly 500 milliseconds after it was produced, but only on quiet topics. On busy topics the delay disappears. The cause is long polling combined with a large min bytes setting: the broker waited for more data that never came, and the timer released the request. Lowering the min bytes back to its default removed the delay. The lesson: if latency is a suspiciously round number, look at the fetch wait settings first.
7Size Limits: How Much Data Comes Back
Three different limits shape every response, and one clever rule stops them from blocking progress.
If a consumer could ask for unlimited data, one busy partition could fill its memory and starve the others. So the fetch protocol has several size limits that work together. Each one protects a different thing.
| Limit | Applies to | Typical default for consumers | What it protects |
|---|---|---|---|
fetch.max.bytes | The whole response | About 50 megabytes | Client memory and network buffers per request |
max.partition.fetch.bytes | One partition inside the response | About 1 megabyte | Fairness, so one partition cannot take everything |
fetch.min.bytes | Minimum before answering | 1 byte | Efficiency, by avoiding tiny responses |
max.poll.records | Records returned to the application per poll | 500 records | Application processing time per loop |
Followers have their own matching settings on the broker side, such as replica.fetch.max.bytes for each partition, replica.fetch.response.max.bytes for the whole response, replica.fetch.min.bytes and replica.fetch.wait.max.ms. They do the same jobs for replication traffic.
The “at least one batch” rule
Here is a subtle problem. Suppose a partition holds a single batch of 3 megabytes, and the per-partition limit is 1 megabyte. If the broker obeyed the limit strictly, it could never send that batch, and the consumer would be stuck forever. Older Kafka versions really did suffer from this. The fix is a rule that is now part of the protocol: if the first batch in the first non-empty partition is larger than the limits, the broker still sends that one batch. The limits are therefore soft for the first batch and strict for everything after it.
Do not treat fetch.max.bytes as a hard cap on memory use. A single oversized batch can exceed it, and a client with many brokers can have several responses in flight at once. Size your client’s memory with that in mind, and keep the largest message size under control so that batches stay reasonable.
A delivery van has a weight limit for the whole trip and a smaller limit for each customer’s order, so no single customer can fill the van. But if one customer orders a single heavy sofa that is over the limit, the company still delivers it alone, because otherwise the sofa would never arrive.
Fairness between partitions
With a shared budget for the whole response, the order in which partitions are served matters. If the broker always started with the same partition, that partition could use up the budget every time, and later partitions would starve. Kafka avoids this by rotating the order of partitions from one request to the next, so that every partition gets its turn at the front of the line. Fetch sessions, which we meet soon, made this rotation cleaner.
8Inside the Broker: How a Fetch Is Served
Follow one request from the network cable to the disk and back, and see why Kafka reads are so cheap.
When a fetch request reaches a broker, it does not go straight to the log. It passes through a small assembly line made of separate groups of threads, each doing one job. This design keeps slow work from blocking fast work, and it is the reason a broker can serve thousands of clients at the same time.
flowchart LR CL["Client or Follower
sends Fetch request"] --> NT["Network Thread
reads bytes from the socket"] NT --> RQ["Request Queue"] RQ --> IO["Request Handler Thread
checks leader and epoch"] IO --> LG["Log Read
find segment and offset"] LG --> DEC{"Enough data for
min bytes?"} DEC -->|"Yes"| RS["Response Queue"] DEC -->|"No"| PU["Purgatory
wait for data or timer"] PU --> RS RS --> NT2["Network Thread
sends bytes to the socket"] NT2 --> CL
A network thread receives the bytes
A small pool of network threads, three by default, reads requests from many connections and puts them in a shared queue. They do not read disks.
A request handler thread takes over
A larger pool of I/O threads, eight by default, picks requests from the queue. It checks that this broker really leads the partition and that the leader epoch in the request is not out of date.
The log is searched by offset
The broker finds the right segment file by comparing the offset with each segment’s starting offset, then uses a sparse index to jump close to the exact position in that file.
The data is handed to the network layer
Whole batches are read from the file. When possible, the operating system copies them straight from its file cache to the network card, without passing through the broker’s own memory.
If there is not enough data, the request waits
The handler thread is freed at once, and the request sits in purgatory until new data, a timer, or a change of leadership completes it.
A network thread sends the response
The response goes back through a queue and a network thread writes it to the socket.
Why reads are so cheap: page cache and zero-copy
Kafka leans heavily on the operating system’s page cache, a part of memory that holds recently used file data. A consumer that keeps up with producers reads records that were written moments ago, so they are almost always still in the page cache, and the disk is barely touched. On top of that, when the data can be sent unchanged, Kafka asks the operating system to copy it directly from the cache to the network. This trick is called zero-copy, and it removes two copies of the data and a lot of CPU work.
Encrypting traffic with TLS forces the data through the broker’s memory, so the shortcut is lost and CPU use rises. Serving an old client that needs the data converted to an older message format has the same effect. Both are worth it in many cases, for security or compatibility, but they should be a conscious choice and not a surprise on the CPU graph.
Warm readers and cold readers
A consumer reading fresh data is a warm reader. A consumer that restarts after a day of downtime and reads old data is a cold reader. Cold reads must fetch old segments from disk, and they push recent data out of the page cache. When that happens on a leader, the fetches from its followers can become slower, followers can slip out of the ISR, and everything gets worse together. This is one reason why a large replay job should be throttled or scheduled carefully.
A restaurant keeps popular dishes ready on the counter, which is the page cache. Regular guests who order today’s specials are served instantly. A guest who asks for a dish from last year’s menu makes the cook run to the far storeroom. If many such guests arrive together, the counter is cleared of the popular dishes to make room, and even the regulars start waiting.
9Follower Fetching and the High Watermark Dance
Replication is just fetch, and the leader learns everything it needs from the follower’s offsets.
On every follower broker, a small group of replica fetcher threads runs all the time. Each thread is responsible for the partitions that follow the same leader broker. It sends fetch requests, appends whatever comes back to the local logs, and goes again. The number of such threads per source broker is set by num.replica.fetchers, one by default. More threads spread the work over more connections, which helps when a broker follows a very large number of partitions.
The clever part is what the follower says. In each fetch request, it states the offset it wants next, and that offset is exactly its own log end offset. So the request itself tells the leader how far the follower has copied. No special progress report is needed. The leader records that number, decides whether the follower is still in sync, and checks whether every in-sync follower now holds a message that was not yet committed. If so, it moves the high watermark forward.
sequenceDiagram autonumber participant P as Producer App participant L as Leader participant F as Follower P->>L: Produce with acks=all Note over L: Append record at offset 10, log end is 11 F->>L: Fetch offset 10 L-->>F: Batch with offset 10, high watermark is 10 Note over F: Append record, log end is 11 F->>L: Fetch offset 11 Note over L: Follower holds offset 10, so high watermark moves to 11 L-->>P: Acknowledgement L-->>F: Empty response after waiting, high watermark is 11 Note over F: Follower now knows offset 10 is committed
Why the follower learns the news late
Look at Figure 4 closely. The leader moves the high watermark only when it receives the follower’s second fetch, and the follower hears about it in the response to that same fetch or a later one. So the follower’s view of the committed line always trails the leader’s by about one round trip. This small lag is normal and is the reason Kafka added leader epochs to fetch. If a leader fails at the wrong moment, a follower’s idea of the high watermark may be a little behind or ahead of the truth, and the epoch is used to sort out any differences safely.
How the ISR uses fetch
The leader also keeps a note of the last time each follower fetched all the way up to the end of the log. This time is what decides ISR membership: if it is older than replica.lag.time.max.ms, the follower is removed. Here is a rule that many people miss. A caught-up follower’s fetch request is parked in purgatory when there is no new data, and it is released when the wait time replica.fetch.wait.max.ms ends. That wait must be much shorter than the lag limit. Otherwise, a healthy follower on a quiet partition could be judged as late simply because its own fetch was waiting politely.
| Setting | Default | Role in replication |
|---|---|---|
num.replica.fetchers | 1 | Number of fetcher threads for each source broker |
replica.fetch.wait.max.ms | 500 ms | How long the leader may hold an empty follower fetch |
replica.fetch.min.bytes | 1 byte | Minimum data before the leader answers a follower early |
replica.fetch.max.bytes | About 1 megabyte | Per-partition size limit for follower fetches |
replica.fetch.response.max.bytes | About 10 megabytes | Total size limit for one follower response |
replica.lag.time.max.ms | 30 seconds | How long a follower may fall behind before leaving the ISR |
Production example: a follower that looks slow but is not
A team lowers its fetch wait for followers to a tiny number to “speed up replication” and sees CPU usage on every broker double, because followers now send a constant stream of near-empty requests. Later they raise the follower wait above the lag limit by accident, and healthy followers on idle partitions start dropping out of the ISR every half minute. The lesson: these values are a balanced pair, so change them together and with evidence.
10Isolation Levels: What a Consumer Is Allowed to See
The same fetch request can show two different views of the log, depending on one field.
Not every record below the high watermark is safe to show to every application. Kafka supports transactions, where a producer writes several records and then either commits them all or aborts them all. While a transaction is still open, its records are already in the log, but the outcome is not yet decided. The fetch request therefore carries an isolation level.
| Isolation level | Reads up to | Sees aborted records? | Best for |
|---|---|---|---|
read_uncommitted | The high watermark | Yes, the client cannot tell | Ordinary apps that do not use transactions, and lowest delay |
read_committed | The last stable offset | No, the client filters them out | Exactly-once pipelines and financial flows |
The last stable offset
The last stable offset, or LSO, is the smaller of two things: the high watermark, and the offset of the first record that belongs to a transaction that is still open. Everything below the LSO has a known fate. A consumer in read-committed mode gets records only up to the LSO, along with a list of aborted transaction ranges, and it discards the aborted records before handing data to the application.
Imagine a shop where customers put items in a basket, and only the ones who pay are counted as sales. The manager who wants confirmed sales only reads the ledger up to the oldest basket that is still open. Baskets that were abandoned are crossed out. The manager who just wants to know what is happening right now reads everything, even items that may later be put back.
If a producer opens a transaction and then stalls, the LSO stops moving. Every read-committed consumer of that partition stops seeing new records, even though the log is growing and the high watermark is advancing. From the outside, it looks like consumer lag is climbing for no reason. Kafka closes forgotten transactions after a timeout, but keeping transactions short is the best cure.
11The Problem Fetch Sessions Solve
Why the plain protocol starts to hurt on large clusters.
Remember that a fetch request lists every partition the client wants to read from that broker. That is fine when the list is short. But large clusters have brokers that follow thousands of partitions from the same leader, and consumers that own hundreds of partitions on one broker. In the original protocol, the client repeated the entire list in every single request, and the broker answered with an entry for every partition again, even if nothing had happened there.
Let us do a rough calculation with round numbers, only to get a feeling. Imagine a broker that follows 4,000 partitions from one leader. Each partition entry in a request needs somewhere around 40 to 60 bytes, once you count the topic reference, partition number, offsets and limits. That is about 200 kilobytes for one request. Now imagine that request being sent and answered many times every second. Most of the partitions in it are quiet, so nearly all of those bytes describe things that did not change. The network, the CPU that reads and writes the entries, and the memory that holds them are all spent on repeating known facts.
The numbers above are illustrations, not measurements, but the pattern is real. Before fetch sessions, the cost of a fetch grew with the number of partitions, not with the amount of work actually done. A cluster with many partitions but modest traffic could spend a surprising share of its resources just on describing itself. Worse, this overhead hit the replication path, so followers could fall behind and leave the ISR because the leader was busy processing giant request lists.
The idea behind the fix
The fix came from a simple observation. Between two consecutive requests, almost nothing changes. The client wants the same partitions, with the same limits, and only a few of them have moved to a new offset. If both sides remember the last full list, the client can send only the differences. This is exactly what happens when you edit a document and save only the changes rather than the whole file. Kafka proposal KIP-227 turned this observation into fetch sessions, and they arrived in Kafka 1.1.
12How Fetch Sessions Work
A session is a memory shared by one client and one broker. Two numbers, a session ID and an epoch, keep them in step.
A fetch session lives between one client and one broker. The broker keeps a small record for the session that holds the list of partitions being fetched, along with the last state it told the client about each of them, such as the last high watermark and log start offset. The client keeps a matching record. Because both sides remember the same list, they can talk in differences.
The three kinds of fetch request
| Kind | Session ID | Session epoch | What it lists |
|---|---|---|---|
| Full fetch that starts a session | 0 | 0 | Every partition the client wants |
| Incremental fetch | The ID the broker gave | 1, 2, 3 and so on | Only partitions whose settings changed, plus a list of partitions to forget |
| Session-less fetch | 0 | -1 | Every partition, and no session is kept |
The client starts with a full fetch
It sends the complete partition list with session ID 0 and epoch 0. This is a polite request: “Please create a session for me if you have room.”
The broker creates the session
It answers the full fetch as usual and adds a new session ID to the response. From now on, that ID stands for the whole partition list.
The client sends incremental requests
Each next request carries the session ID and the epoch, raised by one each time. It mentions only partitions whose fetch offset moved or whose limits changed, plus any partitions the client wants to drop.
The broker replies only about what matters
The response includes partitions that have new records, a changed high watermark or log start offset, a new suggested replica, or an error. Quiet partitions are simply left out.
Either side can end the session
The client can close it by sending the final epoch, -1. The broker can also throw it away at any time, for example when it needs room. The client then simply starts again with a full fetch.
sequenceDiagram autonumber participant C as Client participant B as Broker C->>B: Full fetch, session 0, epoch 0, 1000 partitions B-->>C: Data for 40 partitions, new session id 77 C->>B: Incremental, session 77, epoch 1, 40 changed partitions B-->>C: Data for 12 partitions only C->>B: Incremental, session 77, epoch 2, 12 changed partitions B-->>C: Empty response after the wait Note over B: Broker evicts session 77 to make room C->>B: Incremental, session 77, epoch 3 B-->>C: Error, session id not found C->>B: Full fetch again, session 0, epoch 0 B-->>C: Response with a new session id
What the epoch protects against
The session epoch is a step counter that must go up by exactly one on each request. It protects the shared memory from getting out of step. If a request is lost or repeated, or if two threads mistakenly use the same session, the broker sees an epoch that does not match and refuses the request with an invalid-epoch error. The client then discards its session and begins a fresh one. It is a small price for making sure that “same as last time” always means the same thing to both sides.
A fetch session is like a standing order at a bakery. On day one you give the full list. After that you only say, “Same as yesterday, but two more loaves of rye and no more croissants.” The baker’s notebook and your own notebook must agree on what “yesterday” meant. The epoch is the page number both of you write on each order, so that a missing or repeated page is noticed at once.
What counts as a change
A partition needs to appear in an incremental request when its fetch offset has moved, which happens whenever the client has consumed data from it. It also appears when the client changes its size limit, its leader epoch view, or other per-partition settings. An idle partition that the client keeps asking about at the same offset does not appear at all. On the broker side, a partition is included in the response when it returns records, when its high watermark, last stable offset or log start offset changed since the last response, or when there is an error or a new preferred replica to report.
13The Session Cache: Limited Room and Who Gets It
A session costs the broker memory, so the number of sessions is capped. Understanding the cap explains many odd production behaviours.
Each fetch session stores a list of partitions on the broker, so the broker cannot keep an unlimited number of them. It holds sessions in a session cache with a fixed number of slots, controlled by the broker setting max.incremental.fetch.session.cache.slots. The default is 1,000. One slot holds one session, and one session belongs to one client and one broker. A consumer that reads from five brokers therefore uses five slots, one on each broker.
Who gets a slot when the cache is full
Kafka treats two groups of sessions differently. Sessions opened by follower brokers are privileged. Replication is essential to the health of the cluster, so these sessions may push out ordinary sessions if the cache is full. Sessions opened by consumers are unprivileged. A new consumer session may take a slot from an older one, but the cache aims to protect sessions that were used recently and are large, and it prefers to remove ones that are small and idle. In simple words, the cache tries to spend its limited room where it saves the most work.
The broker simply answers a full fetch without creating a session, and it returns session ID 0 to say so. The client carries on sending full fetch requests, and everything still works. The system degrades gracefully: it becomes heavier, not broken. That is why a full cache often shows up as higher CPU and network use, and not as errors.
How to size the cache
A practical way to size it is to count the clients that will talk to a broker at the same time and add some headroom. The count should include every consumer instance that reads from it, every follower fetcher thread from other brokers that follow its partitions, and any tools or connectors. If you run thousands of small consumer applications, the default of 1,000 can be too small, and constant eviction may cost more CPU than it saves. Raising the number costs some memory, so pick a value from measurements, not from guesses.
Illustrative example: the busy microservice fleet
Picture a retail platform that runs 3,000 small consumer instances, and most of them read at least a few partitions from each of ten brokers. Every broker is asked for about 3,000 sessions, but it only has 1,000 slots. Sessions are constantly created and evicted, and many clients fall back to full fetches. The brokers show high request-handling time and a rising eviction rate. Doubling or tripling the slot count, and reducing needless consumer instances, brings the eviction rate back to nearly zero and cuts broker CPU. The lesson: watch the session cache metrics as your consumer fleet grows.
The restart stampede
Sessions live in a broker’s memory only. When a broker restarts, every session on it disappears. All the clients that were connected to it now receive session errors at about the same moment and reconnect with full fetch requests together. In a large cluster this stampede of full requests can briefly load a freshly started broker much more than usual. Rolling restarts, spaced out and monitored, keep the effect small, and healthy clients back off slightly between retries.
Setting the slot count to zero disables incremental sessions, and every fetch goes back to listing every partition. On a small cluster you may never notice. On a cluster with many partitions and many followers you will pay for it in network use, CPU and replication delay. Treat the value as a capacity setting, not as a feature switch.
14Errors and Recovery in Fetch
Fetch is built for a world where brokers move, leaders change and sessions vanish. Each error has a planned reaction.
A fetch response can carry an error for the whole request or for individual partitions. Most of these errors are not disasters. They are signals that the client’s picture of the cluster is out of date, and the client is expected to refresh that picture and try again. Good clients do this automatically, with small waits between attempts so that they do not hammer a broker that is busy recovering.
| Error | What it means | What the client does |
|---|---|---|
| Not leader or follower | This broker no longer leads that partition | Refreshes cluster metadata and sends the request to the new leader |
| Offset out of range | The requested offset is older than the log start or newer than the log end | Consumer follows its reset policy. A follower truncates or restarts from the leader’s start |
| Fenced leader epoch | The client used an older leader epoch than the broker knows | Refreshes metadata, then retries with the current epoch |
| Unknown leader epoch | The client used a newer epoch than this broker has heard of | Waits briefly, because the broker is catching up on the change |
| Unknown topic or partition | The topic was deleted or never existed here | Refreshes metadata, and reports an error if it stays missing |
| Invalid fetch session epoch | The step counter of the session did not match | Drops the session and sends a full fetch |
| Fetch session ID not found | The broker evicted or lost the session | Drops the session and sends a full fetch |
| Storage error | The broker’s disk had a problem | Backs off, and the platform team should investigate the broker |
Backoff: being a good citizen
When errors appear, retries must be spaced out. Consumers use settings such as retry.backoff.ms and the reconnect backoff settings to wait a little between tries. Followers use replica.fetch.backoff.ms. Without spacing, a broker that is already stressed would receive a flood of repeated requests from every client at once, which makes recovery slower. Backoff turns a crowd rushing a door into a queue.
A few session errors after a broker restart or a rebalance are normal. A constant flow of them, however, means that sessions are being evicted faster than they are useful. Read them as a hint to look at the session cache metrics in chapter 20.
Metadata refresh and the cost of being wrong
Almost every fetch error leads to the same next step: ask the cluster who leads what, right now. Clients keep a cached map of partition leaders and refresh it when errors occur and also on a regular timer. If many clients refresh at once after a leader change, the metadata requests themselves add load. This is another reason why stable clusters with few leader changes are so much easier to run.
15Leader Epochs in Fetch: Catching Stale Views and Diverging Logs
Two small fields let the fetch protocol detect that a log has split from the leader’s log, even when nothing looks wrong.
The leader epoch is a number that goes up every time a partition gets a new leader. Fetch requests carry two epoch values. The first is the current leader epoch, meaning the era the client thinks is in force. The second is the last fetched epoch, meaning the era of the newest record the client has actually received. The broker compares both against its own records.
flowchart TD R["Fetch request arrives
with current epoch and last fetched epoch"] --> Q1{"Client epoch older
than broker epoch?"} Q1 -->|"Yes"| E1["Reply: fenced leader epoch
client must refresh metadata"] Q1 -->|"No"| Q2{"Client epoch newer
than broker epoch?"} Q2 -->|"Yes"| E2["Reply: unknown leader epoch
client waits and retries"] Q2 -->|"No"| Q3{"Does the client log match
this leader at last fetched epoch?"} Q3 -->|"No"| E3["Reply with diverging epoch
client truncates to the given point"] Q3 -->|"Yes"| OK["Serve records from the fetch offset"]
Stale views: the first pair of checks
If the client’s epoch is older than the broker’s, the client is looking at an outdated cluster map, perhaps because a leader change happened while it was busy. The broker refuses the request and the client refreshes its metadata. If the client’s epoch is newer, then the broker is the one that is behind. It has not yet heard about the latest leader change, so the client waits and tries again. These two checks stop a request from being served by a broker that no longer has authority over the partition.
Diverging logs: the second check
The last fetched epoch solves a harder problem. After certain failures, especially an unclean leader election, a follower or consumer may hold records that the new leader never had. Their logs have diverged at some offset. Without help, the reader would keep asking for the next offset and might either get an error far too late or, worse, quietly continue on top of different data. With the last fetched epoch, the leader can look at its own epoch history, see that the client’s last record belongs to an epoch that ended earlier than the client thinks, and reply with a diverging epoch hint: “Your log and mine split here.”
A follower reacts to this hint by cutting its log back to the split point and continuing from there. A modern consumer reacts by either resetting to that point or raising a truncation error, depending on its reset policy. Either way, the problem is caught on the first fetch after the split, not weeks later in a data-quality report. This feature arrived through KIP-320 for consumers in Kafka 2.1, and it works hand in hand with the leader-epoch truncation described in our guide on unclean leader election.
Two friends copy a story from a shared notebook, and each chapter of the notebook is stamped with an edition number. One friend returns and says, “My last page is from edition 3.” The keeper of the notebook replies, “Edition 3 ended on page 40 in my copy, and your extra pages after that are not in the real story.” Because the friend mentioned the edition, the mismatch is found immediately, instead of after ten more pages have been copied on top of the wrong ones.
16Fetching from Followers: Rack-Aware Reads
Sometimes the nearest copy of the data is not the leader. Kafka can let consumers read from it.
In the original design, consumers always read from the leader. If your brokers are spread over three cloud zones, two out of three consumers will typically sit in a different zone from the leader. Every byte they read then crosses a zone boundary, which adds delay and, on most cloud providers, adds a data transfer charge. For a topic read by many consumer groups, this cost can become one of the largest lines on the bill.
Kafka 2.4 introduced fetching from the closest replica, described in KIP-392. Brokers are labelled with their rack or zone using broker.rack, and consumers announce their own location with client.rack. A broker-side plug-in, selected by the replica.selector.class setting, decides which replica is closest. Kafka ships with a rack-aware selector for exactly this purpose. When it is enabled, the leader answers a consumer’s fetch with a hint called the preferred read replica, and the consumer then sends future fetches to that follower instead.
flowchart LR
subgraph ZA["Zone A"]
LD["Broker 1
Leader"]
end
subgraph ZB["Zone B"]
FL["Broker 2
Follower"]
CS["Consumer
client.rack = Zone B"]
end
CS -->|"1. First fetch"| LD
LD -->|"2. Response names preferred read replica: Broker 2"| CS
CS -->|"3. Later fetches stay inside Zone B"| FL
LD -->|"Replication fetch"| FL
What you give up
Benefits
- Much less cross-zone traffic and a smaller network bill.
- Lower read latency for consumers that sit far from the leader.
- Less load on leaders, which can then focus on writes and replication.
- Works with the same protocol, so no changes to applications beyond one client setting.
Trade-offs
- A follower learns the high watermark a little later than the leader, so reads can be slightly behind.
- More planning is needed to keep a healthy replica in every zone.
- If the follower falls behind, a consumer may be sent back to the leader.
- Producers and followers still talk to the leader, so write traffic is unchanged.
Production example: a widely read topic
A media company sends its event stream to twenty consumer groups spread across all three zones of its cloud region. Before rack-aware reads, most of those groups read across zones, and the network cost of this one topic grew every month. After labelling brokers and consumers with their zones and turning on the rack-aware selector, most reads stayed local and the cross-zone charges dropped noticeably. The team kept one extra consumer group on the leader for strict freshness and accepted a little more lag for the others.
A consumer reading from a follower can see only what that follower knows is committed. Since the follower hears about commits one round trip after the leader, the view can be a little older. For dashboards and analytics this is fine. For a flow where every millisecond of freshness matters, keep those consumers on the leader.
17Tuning Fetch: Throughput Versus Latency
A handful of settings decide whether your consumers are quick or efficient. Usually you cannot have both at the maximum.
Every tuning choice in fetch is a version of one question: should the broker answer sooner with less or later with more? Answering sooner gives lower delay for each record. Answering later gives fewer, larger responses, which use less CPU and network per byte. The table below shows the main dials for consumers.
| Setting | Turn it up to | Turn it down to | Watch out for |
|---|---|---|---|
fetch.min.bytes | Get bigger, more efficient responses | Get records the moment they exist | A high value on a quiet topic adds waiting up to the max wait |
fetch.max.wait.ms | Cut the number of empty polls | Bound the worst-case delay | Must stay below the client’s request timeout |
max.partition.fetch.bytes | Read large batches from one partition | Share the response evenly between partitions | Memory use grows with partitions times this value |
fetch.max.bytes | Reduce the number of round trips | Limit memory per response | Several brokers can respond at the same time |
max.poll.records | Process more per loop | Keep each loop short | Long loops risk missing the poll deadline and triggering a rebalance |
receive.buffer.bytes | Fill fast, long network paths | Save memory | The operating system may adjust the value |
Three starting profiles
Default profile
Keep the defaults of one byte minimum and half a second maximum wait. It is right for most services and needs no special reasoning.
Fast profile
Keep the min bytes at one, use a short wait, and process small batches. Suitable for alerts, live pricing and interactive features.
Bulk profile
Raise min bytes to tens of kilobytes or more, allow larger partition limits, and accept slightly higher delay. Suitable for analytics and data lake loads.
Use the client metrics for fetch rate, average fetch size and fetch latency to check the effect of a change. If the average fetch size is tiny and the fetch rate is huge, your consumers are asking too often. If the fetch latency is close to the max wait, the broker is mostly waiting for data, which is healthy on a quiet topic.
Do not fight the batch size
Producers control how large the stored batches are. If producers send many tiny batches, no fetch setting can make reads efficient, because the broker must still handle many small pieces. Tuning the producer side with a short linger time and a reasonable batch size often improves fetch efficiency more than any consumer setting does.
18Performance and Scalability
How fetch behaves as your cluster grows in partitions, clients and traffic.
Fetch scales well because it is simple and because Kafka lets the operating system do most of the heavy lifting. But several resources can still run short, and each one leaves its own fingerprint. Knowing them helps you fix the right thing.
| Resource | How fetch uses it | Sign of trouble |
|---|---|---|
| Network threads | Read requests and write responses for all connections | Low idle percentage on the network processors |
| Request handler threads | Validate requests and read logs | Low idle percentage on the request handlers, long queue times |
| Page cache | Serves recent data without touching the disk | Disk reads climb and fetch latency rises |
| Disk | Serves cold reads and receives writes | Slow follower fetches, ISR shrinks |
| Network bandwidth | Carries responses to consumers and followers | Saturated links, throttled clients |
| Session cache | Remembers partition lists | High eviction rate, many full fetches |
| Memory in clients | Buffers responses from every broker | Out-of-memory errors in consumers with many partitions |
Fan-out multiplies outgoing traffic
A topic that receives 100 megabytes per second and is read by five independent consumer groups sends out about 500 megabytes per second to consumers, plus two more copies to the followers if the replication factor is 3. The outgoing side of a Kafka cluster often carries several times more traffic than the incoming side. When you plan network capacity, always plan for the readers as well as the writers.
Partitions per broker
Each partition a broker leads or follows adds work for fetch: more entries in requests, more files, more state in sessions. Thousands of partitions on one broker are common and fine, but tens of thousands need special care. Fetch sessions were created exactly to keep the cost of many quiet partitions low, yet they do not make partitions free. Keep partition counts reasonable and give every partition a reason to exist.
Quotas keep neighbours polite
Kafka can limit how many bytes per second a client, a user or a group of them may fetch, and how much broker processing time they may use. When a client goes over its quota, the broker delays its responses and reports the delay in the throttle time field. Quotas stop one runaway replay job from stealing bandwidth from everyone else. For replication traffic during partition moves, separate throttle settings exist for the same reason.
Production example: the replay that hurt everyone
A data team restarts a large analytics job that has to reread three days of history. It reads from disk at full speed, pushes recent data out of the page cache, and starves followers of quick reads. Followers begin to lag, the ISR shrinks on several partitions, and unrelated services see slower fetches. Putting a bandwidth quota on the analytics user and scheduling the replay off-peak stops it from happening again.
Newer Kafka versions support tiered storage, where old segments move to cheaper remote storage while recent data stays on the broker’s disks. Reads of very old data can then be served through a separate path, which protects the page cache and the local disks from cold reads. Check the release notes of your version to see how mature the feature is for your use.
19Security and Access Control for Fetch
Fetch is the door through which data leaves the cluster, so it is also the door that must be guarded.
Reading data is a powerful action. A leaked read permission can expose customer records, payments or private messages without any warning. Kafka lets you protect fetch in several layers, and a healthy cluster uses all of them.
Authenticate every client
Require certificates or a trusted login mechanism, so that every fetch request comes from a known user or service.
Grant read per topic
Allow each application to read only the topics it needs. Consumers also need permission on their consumer group.
Protect replica fetches
Followers fetch with a cluster-level permission that only broker identities should hold. Applications should never have it.
Encrypt with TLS
Encrypt traffic between clients and brokers and between brokers. Accept that zero-copy is lost and plan CPU capacity for it.
Use quotas
Limit bandwidth and request time per user, so that no single client can drain the cluster by reading too much.
Log who reads what
Keep authorizer logs, and review broad or unusual read permissions on a schedule.
Mistakes that expose data
The most common security problem with fetch is not a clever attack but a broad permission. A wildcard read permission for a whole team, given “just to make it work”, quietly allows access to every topic, including new ones. Another frequent mistake is sharing one identity between many applications, which makes it impossible to tell who read what. A third is turning off encryption inside a “trusted” network, when a single compromised machine could then listen to all traffic between brokers.
Fetch sessions and abuse
Because sessions use limited memory, a badly behaved or malicious set of clients could try to fill the session cache and force other clients into heavier full fetches. Kafka’s cache rules favour replication and recently active sessions, and connection limits and quotas add further protection. Still, the safest position is to allow only authenticated clients, watch the eviction metrics, and set connection limits per address where it makes sense.
Protecting the fetch door does not protect what the reader does with the data. Keep personal data out of message keys and headers where possible, mask what you can before it enters Kafka, and set retention so that old sensitive records do not stay readable forever.
20Monitoring, Logging and Metrics
Which numbers to watch, what they mean, and the one metric that surprises almost everyone.
Brokers and clients both publish metrics about fetch. The names below are the ones commonly used in Kafka’s monitoring interface. They can differ slightly between versions, so confirm them in the documentation of the version you run.
Broker-side metrics
| Metric | What it tells you | Suggested action |
|---|---|---|
| Request total time for Fetch consumer and Fetch follower | End-to-end time for each kind of fetch | Track the trend, and compare to the max wait setting |
| Request queue time | How long requests wait for a handler thread | Alert when it grows, add handler capacity or reduce load |
| Request remote time | Time spent waiting in purgatory for a fetch | High values are normal for long polling. Do not alert on it alone |
| Request handler idle percentage | How busy the I/O threads are | Alert when it stays low |
| Network processor idle percentage | How busy the network threads are | Alert when it stays low |
| Fetch purgatory size | Number of parked fetch requests | Watch for sudden jumps |
| Number of incremental fetch sessions | How many slots are in use | Compare to the configured slot count |
| Incremental fetch session evictions per second | How often sessions are thrown out | Alert on a steady non-zero rate |
| Replica fetcher maximum lag | How far the slowest follower trails | Alert on growth, since it precedes ISR shrinks |
| ISR shrinks and expands per second | How often followers leave and rejoin | Alert on unusual or repeating patterns |
For consumer fetches, the “remote time” part of the request timing includes the time a request sits in purgatory, waiting for data. On a quiet topic, this is close to the max wait, and that is exactly how long polling should look. Many teams raise false alarms by treating a large remote time as a slow broker. Judge fetch health by queue time, handler idle percentage and follower lag instead.
Client-side metrics
| Metric | What it tells you |
|---|---|
| Fetch rate | How many fetch requests per second the consumer sends |
| Fetch latency average and maximum | How long fetches take from the client’s point of view |
| Fetch size average and records per request | Whether responses are large and efficient or tiny and wasteful |
| Bytes consumed rate | Read throughput of the consumer |
| Fetch throttle time | How much the consumer is slowed by quotas |
| Records lag maximum | How far the consumer is behind the newest record |
flowchart LR BM["Broker metrics
queue time, idle percentage, sessions, follower lag"] --> COL["Metrics Collector"] CM["Client metrics
fetch rate, size, latency, lag"] --> COL COL --> DASH["Dashboards"] COL --> AL{"Thresholds crossed?"} AL -->|"Yes"| ON["Alert the on-call engineer"] AL -->|"No"| REV["Weekly capacity review"] ON --> RB["Runbook: find the layer that is slow"]
Logs
Kafka can log every request, including fetch requests, at a detailed level. This is very useful for a short investigation, but it produces a huge volume of lines on a busy cluster, so turn it on for a short time and then off again. Broker logs also record follower fetch problems, such as errors talking to a leader, and messages about truncation. Client logs show metadata refreshes, session resets and offset resets. Learn what a calm cluster’s logs look like, so that unusual lines stand out.
21Data Flow and Lifecycle: One Consumer Read from Start to Finish
Now that you know the pieces, let us watch a real consumer use all of them in a loop.
A consumer application does not send a single fetch and stop. It lives in a loop that repeats for hours or months. Each turn of that loop connects several ideas from this guide: metadata, positions, fetch requests, sessions, buffering and offset commits. Seeing the loop as one story makes it much easier to find where something goes wrong.
flowchart TD A["Start: get cluster metadata
learn which broker leads each partition"] --> B["Join the consumer group
receive a list of partitions"] B --> C{"Is there a committed offset
for each partition?"} C -->|"Yes"| D["Start at the committed offset"] C -->|"No"| E["Apply auto.offset.reset
earliest or latest"] D --> F["Send one fetch request per broker
full first, then incremental"] E --> F F --> G["Broker waits, then replies with batches"] G --> H["Client buffers the batches"] H --> I["poll returns up to max.poll.records"] I --> J["Application processes the records"] J --> K["Commit offsets"] K --> F
Step by step through the loop
Learn the map
The consumer asks any broker for cluster metadata. The answer lists every partition and its leader, so the consumer knows where to send each fetch.
Get partitions from the group
The group coordinator, another broker role, tells this consumer which partitions it owns. Fetch only ever concerns those partitions.
Find the starting point
For each partition, the consumer looks up the last committed offset. If there is none, its reset policy chooses between the oldest and the newest record.
Send fetch requests, one per broker
The consumer groups its partitions by leader, or by preferred replica if rack-aware reads are on, and sends one request to each broker. The first request to a broker is a full fetch that opens a session.
Buffer the answer
Responses arrive in the background and are stored in a buffer. The application thread is not blocked while the network does its work.
Hand records to the application
When the application calls poll, the client decompresses batches, skips records before its position, drops aborted records if it reads committed data, and returns up to the configured number of records.
Process and commit
The application does its work and records its progress by committing offsets. The next fetch begins from the new position, now as a small incremental request.
Backpressure comes for free
The consumer does not ask a broker for more data while it still holds unprocessed data for that broker’s partitions. If your application is slow, the buffer stays full, the client sends fewer requests, and the network naturally calms down. No special flow-control message is needed. This is one of the quiet benefits of pull-based fetching, and it protects both the consumer’s memory and the broker.
What a rebalance does to a session
When group membership changes, the partitions a consumer owns change too. The consumer then stops reading some partitions and starts reading others. In session terms, the partitions it lost are placed in the forgotten list of the next incremental request, and the partitions it gained appear as new entries. If the change is large, the client may simply drop the session and begin with a fresh full fetch. Either way, the session adapts without any special ceremony.
If the time between two calls to poll grows too long, the group may decide that the consumer has died and hand its partitions to someone else. Fetching itself may be perfectly healthy in that case. Always compare the time your application spends processing a batch against the poll deadline before blaming the broker.
Picture a mail sorter at a desk with a tray. A clerk keeps refilling the tray from the post office, but only while the tray is not full. The sorter takes a handful of letters at a time and works through them, then records how far she got. If she works slowly, the clerk simply waits with the next bag and does not pile more mail onto her desk.
22Failure Scenarios and a Troubleshooting Playbook
Real problems with fetch tend to repeat. Here are the common ones and how to tell them apart.
| Scenario | Typical cause | What you see | First fix to try |
|---|---|---|---|
| Restart stampede | A broker restarted and all its sessions vanished | Short spike in full fetches, CPU and session errors | Space out restarts and let clients back off |
| Session cache exhausted | More clients than slots | Steady evictions, high handler load | Raise the slot count, remove needless clients |
| Followers falling behind | Slow disks, cold reads, network limits | Growing replica lag and ISR shrinks | Throttle heavy readers, fix disk or network |
| Exact half-second delay | High min bytes on a quiet topic | Latency equal to the max wait | Lower min bytes or the wait |
| Read-committed stall | A long or forgotten transaction | Lag grows though the log grows too | Find and end the transaction, shorten transactions |
| Consumer memory pressure | Too many partitions times large per-partition limit | Out-of-memory errors | Lower the limits or spread partitions over more consumers |
| Bouncing between replicas | Rack-aware follower lagging behind | Offset errors, then a return to the leader | Fix follower lag, or keep this group on the leader |
| Request queue backing up | Too few handler threads or too much load | Long queue time, low idle percentage | Reduce load or add capacity |
| Leadership churn | Unstable brokers or network | Many not-leader errors and metadata refreshes | Stabilize the brokers, then look at clients |
flowchart TD
S["Consumers are slow or lagging"] --> A{"Is fetch latency high
on the client?"}
A -->|"No"| B["Look at the application
processing time and poll loop"]
A -->|"Yes"| C{"Is the broker request
queue time high?"}
C -->|"Yes"| D["Broker is busy
check handler idle percentage and evictions"]
C -->|"No"| E{"Is the client throttled
by a quota?"}
E -->|"Yes"| F["Adjust the quota or the workload"]
E -->|"No"| G{"Are disks or page cache
under pressure?"}
G -->|"Yes"| H["Cold reads or slow disks
throttle replays and fix storage"]
G -->|"No"| I["Check the network path
and leadership changes"]
A simple order of investigation
Decide which side is slow
Compare client fetch latency with broker request times. If the client is slow but the broker is fast, suspect the network or the application loop.
Check for throttling
A non-zero throttle time means a quota, not a fault, is slowing the client.
Read the broker queues
Long queue times and low idle percentages on handler threads point to a busy broker.
Look at the session cache
A steady stream of evictions means clients keep falling back to full fetches.
Look at storage
Rising disk reads and a shrinking page cache signal cold readers or slow disks.
Look for leadership churn
Repeated leader changes cause a chain of errors and metadata refreshes in every client.
Change one thing and measure
Fix a single suspect at a time, and confirm with the same metrics that the change helped.
23Design Patterns and Anti-Patterns
Habits that keep reads cheap and predictable, and habits that cause the incidents above.
Patterns that work
Size the session cache on purpose
Count consumers and follower fetcher threads for each broker, add headroom, and set the slot count from that number.
Read near your data
Use rack labels and rack-aware fetching for widely read topics, so most reads stay inside one zone.
Named fetch profiles
Define low-latency, balanced and bulk profiles once, and let teams choose one instead of inventing settings.
Quotas for heavy readers
Give replay and analytics users a bandwidth quota, so that they cannot harm live traffic or replication.
Healthy producer batches
Encourage sensible batching on the write side, which makes every read cheaper as well.
Staggered restarts
Restart one broker at a time and wait for calm metrics before continuing, to avoid session stampedes.
Anti-patterns to avoid
What people do
They set the maximum wait for consumers or followers to a tiny value to make reads “faster”.
Why it hurts
The broker now answers with empty responses again and again, and clients send a constant stream of requests. CPU use rises on both sides for very little gain in real latency.
Do this instead
Keep a sensible wait and rely on long polling. Reduce latency by fixing the real cause, such as an oversized min bytes value or slow processing.
What people do
They ignore the session cache size while the number of consumer applications keeps growing.
Why it hurts
Sessions are evicted constantly, most fetches become full requests, and brokers burn CPU re-describing the same partitions.
Do this instead
Track the session count and eviction rate, and raise the slot count as part of every capacity review.
What people do
They start a huge historical replay at full speed during business hours with no quota.
Why it hurts
Cold reads flush the page cache, followers slow down, ISRs shrink, and live consumers lose speed.
Do this instead
Throttle the replay, schedule it for quiet hours, and watch follower lag while it runs.
What people do
They alert on high fetch “remote time” and restart brokers whenever it rises.
Why it hurts
That number mostly reflects requests waiting politely in purgatory. Restarting brokers causes real harm, including lost sessions, to fix a problem that does not exist.
Do this instead
Alert on queue time, handler idle percentage, follower lag and evictions, which describe real stress.
24Real-World and Industry Examples
How well-known organizations meet the pressures that fetch and fetch sessions were built for.
LinkedIn: the origin of very large Kafka deployments
LinkedIn created Kafka and has publicly described running very large clusters with many brokers and a very high number of partitions. At that size, the cost of repeating long partition lists in every request becomes visible. The lesson: designs that look harmless on a small test cluster can dominate the resource bill at large scale, which is why the community invested in incremental fetching.
Netflix: large event pipelines with many readers
Netflix has written publicly about pipelines that move very large volumes of events to many downstream systems. When one stream feeds many independent readers, outgoing traffic multiplies, so features such as quotas, careful batching and reading from nearby replicas matter a great deal. The lesson: plan for the reading side of Kafka, because it is usually the bigger side.
Uber: many services, many zones
Uber uses Kafka for trip events, location updates and data for real-time decisions across a very large number of services. Organizations with that many clients and zones need predictable fetch behaviour, session capacity that grows with the fleet, and policies that keep expensive cross-zone reads under control. The lesson: fetch settings are shared infrastructure decisions, not personal choices of individual teams.
Cloud-native startups: paying for zone traffic
Many smaller companies discover that their first large cloud bill contains a surprising line for traffic between zones. A topic read by several teams across all three zones can dominate it. Turning on rack-aware fetching for those topics is often the cheapest saving available. The lesson: a small change in where consumers read can change the economics of a whole platform.
These descriptions summarize the general and public use of Kafka at each organization, and the lessons are common patterns in the industry. Internal settings of any company change over time, so treat the examples as teaching material and check current public engineering posts if you need exact details.
25Best Practices and Common Mistakes
A checklist to carry into your next design review or capacity meeting.
Do
- Keep incremental fetch sessions enabled and size the cache from real client counts.
- Monitor session count and evictions next to CPU and request queue time.
- Keep follower wait settings well below the ISR lag limit.
- Choose fetch profiles per workload and write them down.
- Use rack-aware reads for topics with many readers across zones.
- Apply quotas to replay, backfill and analytics jobs.
- Grant read permission per topic and use separate identities per application.
- Keep transactions short if consumers read committed data.
- Restart brokers one at a time and watch the metrics between steps.
Avoid
- Setting tiny wait times to chase lower latency.
- Setting the session slot count to zero to save memory.
- Alerting on purgatory remote time by itself.
- Giving wildcard read permissions to whole teams.
- Running large unthrottled replays during peak hours.
- Treating
fetch.max.bytesas a hard memory cap. - Changing several fetch settings at once without measuring.
- Leaving every consumer on the leader when zone traffic is costly.
- Ignoring producer batch sizes when reads are inefficient.
Common mistakes in more detail
Mistake one: believing fetch sessions change the data. A session only changes how requests are described. The records, the ordering and the guarantees are identical with or without sessions. If a problem involves missing or repeated data, look at offsets, epochs and commits, not at sessions.
Mistake two: forgetting the follower side. Many teams tune consumers carefully and never look at replica fetch settings. Yet replication is often the busiest fetch traffic on the cluster, and its health decides how safe your data is.
Mistake three: reading more than you need. Consumers that subscribe to a topic and throw most of the records away still cost the cluster the full bandwidth. Filtering at the source, splitting topics sensibly or using compacted views can save far more than any tuning.
Mistake four: treating the client and the broker as separate worlds. The best fixes often need a change on both sides, for instance a producer batching change and a consumer min bytes change together.
26Where Fetch Is Heading
The core idea stays the same, while the edges keep improving.
The fetch protocol has changed steadily over the years by adding fields, not by breaking old ones. Topic IDs now let requests refer to a topic by a unique number, which avoids confusion when a topic is deleted and re-created with the same name. Leader epochs made truncation and divergence detection precise. Rack-aware reads moved data closer to its readers. In clusters without ZooKeeper, the controllers copy their own metadata log using the same pull-based pattern, with extensions for their special needs.
Other developments touch fetch indirectly. Tiered storage moves old data to remote storage, so cold reads no longer disturb local disks in the same way. Newer consumer group protocols move more of the coordination work to the broker, which reduces the disruption caused by rebalances. Proposals for queue-like consumption have been discussed and are arriving in stages. Since these features are at different levels of maturity in different releases, read the release notes for your version before depending on any of them.
Fetch is like a well-designed road junction that has been widened, given signs and fitted with better lights over the years, without ever changing where the roads meet. Drivers who learned the junction long ago still find their way, and new drivers benefit from every improvement.
27Worked Example: Sizing and Tuning for an Imaginary Delivery App
Let us put numbers on a made-up company, so that the ideas turn into decisions.
Imagine a food-delivery company called QuickBite. Its Kafka cluster has six brokers, two in each of three zones. There is one busy topic called order-events that receives about 40 megabytes per second. Six independent consumer groups read it: billing, kitchen tablets, delivery routing, fraud checks, analytics and a customer-notification service. In total, about 1,200 consumer instances run across the company, and most of them read from every broker. Each broker also follows partitions from the other five brokers using two replica fetcher threads per source.
Step one: size the session cache
| Item | Count on one broker | Reason |
|---|---|---|
| Consumer sessions | About 1,200 | Nearly every consumer instance reads from every broker |
| Follower sessions | 10 | Five source brokers times two fetcher threads |
| Tools and connectors | About 50 | Monitoring, mirroring and admin tools |
| Total in use | About 1,260 | Already above the default of 1,000 |
| With 30 percent headroom | About 1,650 | Room for growth and reconnect bursts |
The team sets the slot count to 2,000, which leaves comfortable room. Before the change, the eviction metric showed a steady stream of evictions and many clients running full fetches. After the change, evictions drop close to zero, and broker CPU during peak hours falls.
Step two: cut cross-zone traffic
The topic’s traffic is 40 megabytes per second in, and six groups read it, so about 240 megabytes per second flow out to consumers. Because leaders are spread over three zones, roughly two thirds of reads cross a zone boundary. That is about 160 megabytes per second, which adds up to nearly 14 terabytes per day. These are illustrative numbers, and real prices depend on the provider, but the size is clear. The team labels brokers with broker.rack, sets client.rack on the consumers, and enables the rack-aware replica selector. The delivery routing group, which needs the freshest data, stays on the leader by using a client without a rack label. The others read locally and accept a tiny extra delay.
Step three: give each group a profile
| Consumer group | Profile | Reason |
|---|---|---|
| Delivery routing | Low latency, reads from leader | Every second counts for drivers and customers |
| Kitchen tablets | Balanced, rack-aware | A short delay is acceptable |
| Customer notifications | Balanced, rack-aware | Small delay is fine |
| Fraud checks | Low latency, read committed | Needs decided transactions and quick reaction |
| Billing | Balanced, read committed | Must never process aborted records |
| Analytics | Bulk, rack-aware, bandwidth quota | Throughput matters more than delay, and it must not disturb live traffic |
The result
After the three changes, QuickBite’s brokers show fewer full fetches, lower request queue times at peak, and a much smaller cross-zone bill. On the next restart night, the team staggers broker restarts and sees only a small, short bump in session errors, exactly as the runbook predicted.
28Frequently Asked Questions
fetch.min.bytes and fetch.max.wait.ms.max.incremental.fetch.session.cache.slots. Large fleets often need more.fetch.max.bytes strictly limit the response size?29Summary and Key Takeaways
Everything in this guide can be held in a few sentences.
Key Takeaways
- One request, two readers. Consumers and follower brokers both use Fetch. The replica ID tells the leader which one it is serving, and follower offsets drive the high watermark and the ISR.
- Long polling is deliberate. The broker parks a request in purgatory until enough bytes arrive or the wait ends, which avoids floods of empty answers.
- Limits are soft for the first batch. Size limits protect memory and fairness, but the first batch is always sent, so nobody gets stuck.
- Sessions make cost follow change. A full fetch opens a session, incremental fetches list only what changed, and epochs keep both sides in step.
- The session cache is a capacity setting. The default of 1,000 slots is often too small for big fleets. When full, Kafka degrades to full fetches, so watch the session count and evictions.
- Epochs keep reads honest. Leader epochs in fetch catch stale views and diverging logs early, and rack-aware reads move data closer to consumers to save cost.
- Watch the right numbers. Judge fetch health by queue time, handler idle percentage, follower lag and evictions, not by long-poll wait time, and tune one setting at a time.