Diagnosing Broker GC Pauses in Kafka
Why a healthy-looking Kafka broker can freeze for a few seconds, how those freezes cascade into leader elections and client errors, and a step-by-step method for finding the real cause instead of guessing.
Imagine a busy restaurant kitchen where, every so often, the manager shouts “everybody stop!” and the whole team freezes while a cleaner sweeps the floor. Orders keep arriving at the door, but nothing moves until the sweeping ends. Most freezes last a blink. Sometimes one lasts long enough that the waiters think the kitchen has burned down and send customers to another restaurant. A Java garbage collection pause on a Kafka broker is exactly this freeze.
Kafka is written for the Java Virtual Machine, so every broker carries a garbage collector that periodically stops the program’s threads to tidy memory. Usually you never notice. When it goes wrong, brokers fall out of the cluster, replicas drop out of sync, producers see timeouts and on-call engineers see graphs that make no sense. This guide starts from zero, explains what garbage collection is and how Kafka uses memory, and then teaches a calm, repeatable way to find out whether a pause is really a garbage collection problem, what caused it and how to fix it.
1Why GC Pauses Matter So Much for Kafka
Kafka is a system built on promises of speed and steady behavior, and a pause quietly breaks both.
A quick picture of Kafka
Apache Kafka stores streams of events in named containers called topics, and each topic is cut into partitions. The servers that store partitions are brokers. Programs that write events are producers, programs that read them are consumers, and every partition has several copies called replicas, one of which is the leader that clients talk to. All of these parts constantly send small messages to one another: heartbeats that say “I am alive,” fetch requests that say “give me new data” and acknowledgments that say “I stored it.”
What a pause does to that conversation
A garbage collection pause is a moment when the Java program stops running its own code so the collector can clean up unused memory. During this moment the broker cannot answer requests, cannot send heartbeats and cannot fetch data from leaders. To the rest of the cluster, a broker that is paused looks exactly like a broker that has crashed or lost its network. Only when the pause ends does the difference become clear.
Short pauses of a few milliseconds are harmless. Pauses of a few hundred milliseconds create latency spikes that clients feel as a slow request. Pauses of several seconds can make the cluster believe the broker is dead, and pauses of half a minute can remove it from the in-sync group of a great many partitions at once.
Think of a call centre where every agent must send a “still here” message to the supervisor every two seconds. If a fire drill freezes all agents for ten seconds, the supervisor decides they have all left the building and hands their calls to other agents. When the agents return, they find their work already given away and everyone is confused. A long garbage collection pause is the fire drill.
Why averages hide the problem
Most dashboards show average latency, and averages hide pauses. If a broker serves 10,000 requests per second and freezes for one second, the other 9,999 seconds look perfect. The average barely moves, yet in that one second thousands of requests waited. Engineers who study large systems call this the tail latency problem: the rare slow requests matter more than the typical fast ones, because a user or a service that sends many requests will hit a slow one sooner or later. Diagnosing GC pauses is largely about looking at the tail instead of the average.
A pause is not only “slow.” From the cluster’s point of view, a paused broker is a missing broker. That is why one long pause on one machine can trigger changes across the whole cluster.
2How a Kafka Broker Uses Memory
To understand what the garbage collector is cleaning, first see where a broker keeps its data. Most of it is not where beginners expect.
Three different kinds of memory
A running broker uses memory in three main places, and only one of them is managed by the garbage collector.
The Java heap
The area where Java objects live: request objects, buffers being processed, metadata caches and bookkeeping for partitions and groups. This is what the collector cleans.
Off-heap memory
Memory outside the heap: direct buffers used for network and file operations, memory-mapped index files, thread stacks and the space the JVM keeps for its own code.
The page cache
Free memory that the operating system uses to keep recently written and read log data. Kafka depends on it heavily and does not control it directly.
Why the heap is small on purpose
Kafka does not keep your messages in the Java heap. It appends them to files on disk and relies on the operating system’s page cache to serve recent data quickly. That is why a broker with 64 gigabytes of memory usually runs with a heap of only a few gigabytes. The rest of the memory is left to the page cache, which is much faster and cheaper for Kafka’s needs than a giant heap that the garbage collector must constantly examine.
flowchart TB
subgraph HOST["Broker machine memory"]
subgraph JVM["Broker Java process"]
HEAP["Java heap: young and old areas"]
META["Metaspace: class definitions"]
DIRECT["Direct buffers for network and files"]
STACK["Thread stacks"]
MMAP["Memory-mapped index files"]
end
PC["Operating system page cache holding log data"]
end
CLIENTS["Producers and consumers"] --> DIRECT
DIRECT --> HEAP
HEAP --> PC
PC --> DISK[("Disks with log segments")]
A helpful rule
Give the heap enough room for the broker’s working objects, and give everything else to the operating system. A heap that is too small causes constant cleaning. A heap that is too large steals memory from the page cache, forcing more reads from disk, and makes each cleaning longer. Good sizing sits in between, and Chapter 13 shows how to find that spot.
Picture a library. The heap is the librarian’s desk, where books being handled today are placed. The page cache is the big reading room shelf where popular books stay handy. The disk is the warehouse in the basement. If the librarian’s desk grows so large that it takes over the reading room, readers must run to the warehouse for every book, and tidying the enormous desk takes the librarian longer.
Many teams see “memory used: 95 percent” on the machine and conclude the broker needs more heap. Most of that memory is usually the page cache, which is healthy. Look at heap usage after garbage collection, not at the machine’s total memory.
Budgeting the off-heap part
Because off-heap memory is invisible to the garbage collector, it is easy to forget when planning. Yet the total memory of the Java process is the heap plus several other pieces. Direct buffers grow with the number of connections and the sizes of network reads and writes. Metaspace holds the definitions of classes and grows with the code the broker loads. Every thread has a stack, and a busy broker runs hundreds of threads. Memory-mapped index files add address space that grows with the number of log segments. If you run in a container with a memory limit, all of these plus the page cache count toward the limit, and the operating system may kill the process when it goes over, with no warning from the JVM at all.
A practical planning rule is to add up the heap, a generous allowance for direct buffers and metaspace, and a fair amount of headroom, and then leave the majority of the machine’s remaining memory to the page cache. The exact numbers vary, so confirm them by watching the real process over a busy week, not by guessing.
3Garbage Collection From Zero
Before diagnosing anything, you need a clear mental model of what a garbage collector does and why it sometimes has to stop the world.
What is garbage?
Whenever a Java program needs a piece of data, such as the contents of a request, it asks the JVM for a chunk of heap memory. This creates an object. When the program has finished with the object and nothing refers to it any more, the object becomes garbage: it still occupies memory but is of no use. The garbage collector is the part of the JVM that finds such objects and reclaims their space so it can be reused. Programmers in other languages have to free memory by hand and often forget. Java does it automatically, and the price is occasional pauses.
Why the world must sometimes stop
To find garbage safely, the collector must know which objects are still in use. While it studies the objects, the program must not change who refers to whom, or the collector could throw away something still needed. The simplest way to keep the picture still is to freeze all application threads. This freeze is called a stop-the-world pause, often shortened to STW. Modern collectors do as much work as possible while the program keeps running, but they still need short freezes at certain moments.
Young objects die young
Studies of real programs found that most objects live for a very short time. A request object is created, used within milliseconds and is then garbage. So the heap is divided into two main areas. The young generation is where new objects are born. It is cleaned often and cheaply, because most objects there are already dead and only a few survivors must be copied. The old generation holds objects that have survived several cleanings and are likely to live a long time. It is cleaned less often, but cleaning it costs more.
| Term | Plain meaning | Why it matters for Kafka |
|---|---|---|
| Allocation rate | How fast the program creates new objects | A busy broker with encrypted traffic creates a lot of short-lived objects |
| Live set | The objects still in use after cleaning | Grows with partitions, groups and connections |
| Young collection | A quick clean of the newborn area | Happens many times per minute; should be very short |
| Old or full collection | A deep clean of long-lived objects | Rare when healthy; can cause the longest pauses |
| Promotion | Moving survivors from young to old | Too much promotion fills the old area too quickly |
| Throughput versus latency | Total work done versus how long single pauses last | Kafka cares mostly about short, predictable pauses |
Throughput collectors versus low-pause collectors
Collectors are designed with different goals. Some aim for the highest total work done and accept longer pauses now and then. Others aim for short, predictable pauses and accept spending a little more processor time to achieve them. A message broker is in the second group, because its clients and its cluster peers are always waiting on short deadlines. This is why Kafka guidance has recommended a low-pause collector called G1 for many years.
Cleaning an office can be done in two ways. You can close the office on Sunday and clean everything at once, which is fast overall but everyone is locked out for a long time. Or a cleaner can work a few minutes every hour, tidying one room at a time, which costs a bit more effort but nobody is ever locked out for long. A message broker needs the second style.
There are two knobs that decide how painful garbage collection is: how fast the program makes garbage, and how much long-lived data the collector must examine. Almost every fix in this guide turns one of those two knobs.
Collector threads and the price of speed
Modern collectors use several threads at the same time to speed up their work. During a pause, many threads clean different parts of the heap in parallel, so a machine with more processor cores can finish a pause faster. Some work also runs in the background alongside the application, for example the marking of old objects. This background work uses processor time that the broker would otherwise spend on requests. On a machine that is already busy, the background threads compete with request handling, and both can slow down. The takeaway is that garbage collection is never free. It uses processor time, and a broker with too few cores can suffer pauses that would be short on a bigger machine.
The number of collector threads is normally chosen by the JVM based on the number of cores it sees. In containers and on shared virtual machines, the JVM may see more or fewer cores than are really available. If it starts too many threads for the real processor share, the threads fight each other and pauses stretch. Checking what the JVM believes about the machine is a quick and useful step in any investigation.
4How G1 Works, and the Details That Matter for Kafka
G1 is the collector used by nearly every production Kafka broker. Knowing its rhythm makes GC logs easy to read.
Regions instead of two big blocks
The G1 collector, whose name comes from “garbage first,” splits the heap into many equal-sized pieces called regions. At any moment each region plays a role: some hold young objects, some hold old objects and some are free. Because the regions are small, G1 can clean a few of them at a time and choose the ones that contain the most garbage first, which is where the name comes from. You can ask G1 to aim for a pause target, and it picks how many regions to clean in each step to try to stay within the target.
The rhythm of G1
flowchart LR
Y["Young collection: copy survivors, short pause"] --> Y
Y --> M["Concurrent marking: runs alongside the program"]
M --> MX["Mixed collections: clean young plus some old regions"]
MX --> Y
M -.->|"if old area fills too fast"| F["Full collection: long stop-the-world pause"]
MX -.->|"if space runs out"| F
F --> Y
In a healthy broker, the loop of young collections runs many times per minute. Each one stops the program for a few milliseconds up to a few tens of milliseconds. When the old area of the heap becomes about a certain percentage full, G1 starts a concurrent marking cycle, which studies the old objects while the program keeps running. Afterwards, G1 performs several mixed collections that clean old regions full of garbage together with the young area. The setting that controls when concurrent marking begins is called the initiating heap occupancy percentage, and it plays a central role in tuning.
The dangerous path: the full collection
If G1 falls behind and the heap runs out of free regions before marking and mixed collections have freed enough space, the collector gives up on being clever. It performs a full collection, which stops the entire program and cleans the whole heap. In older Java versions this work was done by a single thread, which made it extremely slow. Even in newer versions it is long compared with normal pauses. Many terrible incidents in Kafka clusters are full collections, so in the GC log, hunting for them is the first job.
Humongous objects
G1 has a special rule for very large objects. Any object that is at least half the size of a region is called humongous, and it is placed in its own group of regions directly in the old area, skipping the young area. Humongous objects can cause trouble: they are not cleaned by cheap young collections, they can fragment the heap because they need several neighboring free regions, and a burst of them can trigger extra marking cycles. Kafka allocates large buffers when clients send big batches or ask for big fetches, so humongous allocations are a real risk. Choosing a larger region size, for example 16 megabytes, makes more of these buffers count as ordinary objects, and this is one reason the Kafka documentation’s sample tuning includes a region size setting.
The pause target is a hint, not a promise
The pause target setting tells G1 how long you would like each pause to be. The default is 200 milliseconds, which is too relaxed for many Kafka clusters. The Kafka operations documentation describes a busy cluster at LinkedIn tuned with a target of 20 milliseconds, where the 90th percentile of pauses was about 21 milliseconds and there was less than one young collection per second. But a very low target has a cost. G1 must clean fewer regions per step, so it needs more steps and more processor time, and if the target is unrealistic for the amount of live data, G1 may fall behind and end up in a full collection. Targets should be set from measurement, not from hope.
| G1 setting (concept) | What it controls | If set too low | If set too high |
|---|---|---|---|
| Pause target | Desired maximum length of a normal pause | More frequent pauses and more processor use, risk of falling behind | Long pauses that clients notice |
| Initiating occupancy percentage | How full the old area gets before concurrent marking starts | Marking runs too often and wastes processor time | Marking starts too late and the heap fills, leading to full collections |
| Region size | Size of each heap region | Many buffers become humongous | Coarser cleaning units |
| Heap size | Total space for objects | Frequent collections, risk of running out | Longer collections and less page cache |
A strong answer to “Why do Kafka brokers use G1?” mentions predictable pause times, region-based incremental cleaning, tuning of the pause target and the initiating occupancy, and the special handling of large objects. A great answer adds that a full collection is the failure mode to watch for.
Evacuation failure and “to-space exhausted” in plain words
During a young or mixed collection, G1 copies the surviving objects from the regions it is cleaning into empty regions. This copying is called evacuation, and the empty regions it copies into are the to-space. If G1 runs out of empty regions in the middle of a collection, it has nowhere left to copy survivors. The log then reports that the to-space is exhausted, and G1 must finish the collection in a slower, more careful way, which produces a much longer pause. In the worst case it follows with a full collection.
The usual reasons are a heap that is too small, a sudden burst of surviving or humongous objects, or marking that began too late so that old regions are still full of garbage. G1 keeps a reserve of free space for this exact case, and tuning the reserve upward is one option, but it is usually better to address the reason for the shortage. In the GC log, phrases about to-space exhaustion or evacuation failure are strong signals that the heap sizing or thresholds need attention.
Imagine moving a shop’s stock into a new warehouse room by room while the shop stays open. You need empty shelves in the new warehouse to place the goods. If the new warehouse fills up before you finish, you must stop the move and sort things out on the spot, closing the shop for much longer than planned. Keeping enough spare shelves avoids this.
5What Fills a Broker’s Heap
Every GC problem comes from one of two sources: too much garbage made too quickly, or too much long-lived data. Here is where each comes from in Kafka.
Sources of fast garbage: the allocation rate
Request and response objects
Every produce, fetch and metadata call creates objects to decode and answer it. Thousands of clients mean a steady stream of short-lived objects.
TLS traffic
Plain connections can send file data straight from disk to the network without touching the heap. Encrypted connections must read data into memory and encrypt it, creating more buffers and more garbage.
Message format conversion
When an old client needs an older message format, the broker must convert batches in memory, which also removes the fast path that avoids copying.
Recompression and validation
If the broker must decompress and recompress batches, or check them closely, it allocates working buffers for each batch.
Big produce and fetch buffers
Large client batches and big fetch responses need big buffers, some of which are humongous in G1 terms.
Follower fetch traffic
Follower brokers fetch constantly from leaders, and each fetch creates objects for every partition it covers.
Sources of long-lived data: the live set
The second group grows slowly and stays. It decides how much the collector must examine in each mixed or full collection.
| Long-lived item | What grows it | Typical warning sign |
|---|---|---|
| Partition and log objects | Number of partition replicas on the broker | Heap after collection rises as partitions are added |
| Metadata cache | Total partitions and topics in the cluster | Slow growth of old area as the cluster expands |
| Consumer group state | Number of groups, members and committed offsets | Heap jumps after a coordinator loads a big offsets partition |
| Transaction and producer state | Number of active producers and transactional ids | Growth linked to idempotent or transactional producers |
| Connection state | Number of client connections, especially encrypted ones | Heap use scales with connection count |
| Delayed operation queues | Requests waiting for acknowledgments or fetch data | Grows during slowdowns, then makes them worse |
Why the two problems feed each other
A vicious circle is common. A pause makes requests wait. Waiting requests pile up in queues, and clients time out and retry, which creates even more requests and more garbage. When the pause ends, the broker faces a bigger burst than before, so the next collection is longer, and the cycle repeats. This is why a small tuning problem can suddenly become a full outage under load.
Picture a supermarket checkout that freezes for a minute. Customers pile up in the queue, some leave and come back, and new ones join. When the checkout restarts, it faces a much longer queue than before. If the next freeze arrives soon, the queue never clears. Garbage pauses under load behave the same way.
Example: the memory cost of turning on encryption
A team enables encrypted connections on all listeners for a compliance audit. Overnight, the young collections on every broker become more frequent and the average pause grows from a few milliseconds to about thirty. Nothing in the client code changed. The broker simply allocates far more memory per byte served, because it can no longer send file data directly to the network. The fix combines a modest heap increase, a check on batch sizes and a review of the collector settings.
6How a Pause Turns Into an Outage
A pause by itself only stops one machine. Timeouts across the cluster are what turn it into a bigger event.
The chain reaction
flowchart LR
GC["Long GC pause on one broker"] --> HB["Heartbeats and fetches stop"]
HB --> TO["Session timeout passes"]
TO --> FENCE["Controller marks broker as dead or fenced"]
FENCE --> LE["Leader elections for its partitions"]
LE --> CE["Client errors and metadata refresh storm"]
GC --> ISR["Followers of its leaders drop out of sync"]
ISR --> WE["Write errors if too few copies remain"]
GC --> RETURN["Pause ends, broker returns"]
RETURN --> CATCH["Catch-up replication and leader rebalance"]
The timeouts that decide how bad it gets
Each connection between two Kafka components has a timeout. If the pause is shorter than the timeout, the effect is only extra latency. If the pause is longer, something is declared dead. Knowing the timeouts lets you predict how much pause your cluster can tolerate. The defaults below are typical for recent versions, but they change between releases and are often customized, so always confirm them in your own configuration.
| Timeout | Typical default | What happens when a pause exceeds it |
|---|---|---|
| Broker session with the KRaft controller | About 9 seconds, with heartbeats about every 2 seconds | The controller fences the broker and moves its leaders away |
| Broker session with ZooKeeper (older clusters) | About 18 seconds in recent versions | The session expires and the broker is removed from the cluster |
| Follower lag limit for staying in sync | About 30 seconds in recent versions | The leader removes the slow follower from the in-sync group |
| Producer request timeout | About 30 seconds | The producer treats the request as failed and retries |
| Consumer group session timeout | Roughly 10 to 45 seconds depending on version | A member can be removed, causing a rebalance |
| Client-side request deadlines set by applications | Often 1 to 10 seconds | Application errors long before Kafka’s own timeouts |
Why a follower’s pause matters as much as a leader’s
If a leader pauses, clients cannot write to its partitions until it wakes or a new leader is chosen. If a follower pauses, the leader may stay perfectly healthy, yet the follower stops fetching. When producers require several in-sync copies before accepting a write, a paused follower can bring the count below the required minimum, and writes to many partitions start failing even though the leader never paused at all.
sequenceDiagram
participant P as Producer
participant L as Leader Broker
participant F as Follower Broker paused
participant C as Controller
P->>L: Produce with all replicas acknowledging
L->>F: Waiting for follower to fetch
Note over F: GC pause of several seconds
F--xL: No fetch requests arrive
L->>L: Follower lag grows past limit
L->>C: Shrink in-sync set
L-->>P: Error if minimum copies not met
Note over F: Pause ends
F->>L: Fetch resumes and catches up
L->>C: Expand in-sync set
The clean-up cost after a pause
Recovery is not instant. After a broker returns, it must catch up on missing data for every partition, rejoin in-sync groups and take back its leader duties through preferred leader election. During this time it uses extra network and disk capacity, which can create another round of pressure if the cluster is already busy. Engineers who only look at the pause itself often miss this second wave of problems.
Do not raise session timeouts as the first response to pause incidents. Longer timeouts hide the symptom by making the cluster slower to notice real failures. Treat them as a short-term safety net while you fix the cause.
Working out your pause budget
A helpful exercise is to compute how long a pause your cluster can survive without visible damage. List the timeouts that matter and pick the shortest one that you cannot change quickly. For many teams it is not one of Kafka’s own timeouts, but a deadline in a client application, for example a payment service that gives up after two seconds. Call that the hard limit. Your target for the longest normal pause should be well below it, say a quarter or less, because some pauses will be longer than average and because the pause is only part of the delay a request experiences.
Then compare with what your logs show. If the longest pause each day is under a quarter of the hard limit, you have healthy headroom. If it is within half of the limit, you are on thin ice. If it exceeds the limit, clients are already failing and the priority is clear. Recording this budget in the team’s documentation gives everyone a shared, numeric definition of “too long.”
| Longest pause compared with the shortest important timeout | Meaning | Action |
|---|---|---|
| Under one quarter | Healthy headroom | Keep monitoring and hold the baseline |
| Between one quarter and one half | Warning zone | Investigate trends and reduce garbage or live data |
| Between one half and the full timeout | Danger zone | Treat as an incident and start the method in Chapter 11 |
| Longer than the timeout | Failures are already occurring | Stabilize first, then find the root cause |
7Recognizing a Pause from the Outside
You usually do not start with a GC log. You start with a strange graph, and the pattern tells you where to look.
Symptoms in Kafka metrics
| What you see | Why it may point to a pause |
|---|---|
| Sudden spikes in the 99th percentile of produce or fetch time | Requests waited while the broker was frozen |
| Request queue time jumps while processing time stays normal | Requests piled up in the queue during the freeze |
| Network and request handler idle percentages drop sharply then recover | Threads were busy catching up after the pause |
| Bursts of in-sync set shrinks followed by expands | Followers stopped fetching, then resumed |
| Short bursts of under-replicated partitions that heal alone | A pause on the leader or follower, not a real failure |
| Leader elections without any broker actually stopping | A broker was fenced for missing heartbeats |
| Client timeouts in a pattern that repeats at similar intervals | A periodic collection cycle or cleanup job |
Symptoms in logs
Broker logs often hold the clearest hints. Look for messages about the broker’s session with ZooKeeper timing out, or, in newer clusters, about the controller fencing the broker or about failing to send heartbeats in time. Look for statements that a follower was removed from the in-sync set, and for warnings from the JVM that the application threads were stopped for an unusually long time. Line up the timestamps of these messages, because the order tells the story: first the freeze, then the missed heartbeats, then the removal.
The signature of a GC pause versus other problems
Everything stops at once
All request types stall together and the processor may look idle during the freeze. Several unrelated clients see errors at the same second.
Slow but moving
Requests are slow but still progress. Disk wait time is high, and fetches for old data suffer more than fetches for recent data.
Errors per connection
Some clients fail while others are fine, and retransmissions or dropped connections appear in network counters.
Steady climb
Latency rises gradually with traffic and falls when traffic falls, with processors and threads busy throughout.
A doctor can tell a fever from a broken bone by how the patient behaves, before running any test. A freeze looks like a heartbeat that stops and restarts. A slow disk looks like a person walking with a limp. Learning the outward signs helps you choose the right test.
Put the 99th and 99.9th percentile request times on the main dashboard, not only the average. Add vertical markers for deployments and restarts. Most pause investigations start by noticing a spike that lines up on one broker only.
One broker or many?
Ask first whether the pattern appears on a single broker or on all of them at the same moment. A single broker suggests something local: a hot partition, too many leaders, a bad disk or a machine problem. All brokers at once suggests something shared: a change in client behavior, a scheduled job, a common dependency, a cloud event or a cluster-wide configuration change. This single question halves the search space.
8Collecting Evidence: GC Logs, JMX and Flight Recorder
You cannot diagnose what you cannot see. Set up these three sources of evidence before the next incident, not during it.
GC logs: the black box recorder
The JVM can write a detailed record of every collection to a file. In Kafka’s standard start scripts, this logging is switched on when the broker runs as a background service, and the file is stored beside the other broker logs, rotated by size. In older Java versions, the settings were named after “print GC details” and a log file path. In Java 9 and later, a single unified logging option controls it and lets you choose which tags to record and how to decorate each line with a date and uptime. Whatever version you run, make sure of four things: the log is on, it includes timestamps, it keeps enough history to cover several days and it is written to a fast local disk.
Writing the GC log to a slow or busy disk can itself extend pauses. Some collections write their log line while the program is stopped, and if the disk stalls at that moment, the whole world waits for the disk. Put GC logs on a fast local device that is not shared with the busy log segments.
JMX metrics: the live dashboard
The JVM exposes counters through JMX. For each collector, it reports how many collections have happened and the total time spent in them. Sampling these values every 15 to 60 seconds and taking the difference gives you collections per second and the share of time spent collecting. Also collect the heap size after collection, the sizes of the young and old areas and the number of threads. If you use a metrics exporter, add these to your standard broker dashboard. On Amazon MSK, some of these are published for you in CloudWatch, for example a metric for the heap in use after collection, but you should check the current list of metrics for your broker type.
Java Flight Recorder: the microscope
Java Flight Recorder, or JFR, is a low-overhead profiling tool built into modern OpenJDK builds. It can record collections, allocations, lock waits, thread stalls and safepoints for a period of minutes and save them to a file that you open in a viewer. It is ideal for questions such as “which parts of the code allocate the most memory?” and “what were threads doing during the stall?” Because the overhead is small, many teams enable a rolling recording so that the last few minutes are always available when an incident strikes.
flowchart TB
JVM["Broker JVM"] --> GCL["GC log files"]
JVM --> JMX["JMX metrics"]
JVM --> JFR["Flight Recorder recordings"]
OS["Operating system counters"] --> COL["Metrics collector"]
JMX --> COL
GCL --> SHIP["Log shipper"]
SHIP --> STORE["Central log store"]
COL --> DASH["Dashboards and alerts"]
JFR --> ANALYSIS["Engineer analysis on demand"]
STORE --> DASH
DASH --> RUNBOOK["Runbook and on-call response"]
Other useful tools
| Tool | What it shows | Caution |
|---|---|---|
| jstat | Live view of heap areas and collection counts | Sampling only, may miss short events |
| jcmd | Heap summary, thread dumps, native memory report, starting recordings | Some commands are heavier than others |
| Class histogram | Which object types use the most memory | The “live” variant forces a full collection, so avoid it on a struggling production broker |
| Heap dump | Complete snapshot of all objects | Freezes the broker while writing and produces a huge file |
| Log viewers such as GCViewer or online analyzers | Graphs and statistics from GC logs | Check whether your log policy allows uploading logs to a public service |
| Allocation profilers | Which code paths create the most garbage | Test in staging before running on production |
Never take a heap dump or run a “live” object histogram on a broker in the middle of an incident unless you can afford to lose that broker for a while. Remove it from service first by moving its leaders away.
Setting up a rolling recording plan
Decide the retention window
Choose how many minutes or hours of recording you want available at any moment, for example the most recent hour.
Start with the low-overhead profile
Use a standard profile that records collections, safepoints and thread stalls without heavy sampling.
Store recordings safely
Write files to a local disk with enough space and copy them off the machine when an incident happens.
Practice opening one
Ensure at least two engineers know how to open and read a recording, before the day it is needed.
9Reading a GC Log
A GC log looks scary at first. In practice you read only a handful of fields.
What each line tells you
Each collection produces a record with a time, a type, a cause, the heap size before and after, and the duration. Your eyes should look for these items in order.
Type of collection
Was it a young collection, a mixed one, a concurrent cycle or a full collection? Full collections deserve immediate attention.
Cause
The log names why the collection started, such as the young area being full, a large allocation, a request for an explicit collection or the class metadata space being full.
Heap before and after
If the heap after collection keeps rising over hours, live data is growing. If it drops back to the same level each time, the live set is stable.
Pause duration
The real elapsed time is what clients feel. Compare it with your timeouts.
User, system and real time
These three numbers separate a collector problem from a machine problem. See below.
Warnings
Phrases such as “to-space exhausted” or “evacuation failure” mean G1 ran out of room while copying objects and had to fall back to slower work.
The three time numbers
Most log formats print three times for each pause. User time is processor time spent in the collector’s own code, added across all its threads. System time is processor time spent inside the operating system on the collector’s behalf, for example handling memory. Real time is the actual clock time that passed. Compare them, because their relationship exposes hidden problems.
| Pattern | What it usually means | Where to look next |
|---|---|---|
| Real time roughly equals user time divided by the number of collector threads | Normal: the collector did real work using its threads | Reduce garbage or live data |
| System time is large | The operating system is slowing memory work, often because of swapping or memory compaction features | Swap settings, transparent huge pages, memory pressure |
| Real time is much larger than user plus system time | Threads were waiting, not working: disk stalls, processor starvation or a noisy neighbor | Disk latency, processor steal or throttling, log disk |
| Real time small but clients saw a long stall | The stall was not in the collector, perhaps a safepoint or operating system event | Safepoint logging and system metrics |
Typical causes and what they suggest
| Cause shown in log | Meaning | Common remedy |
|---|---|---|
| Young area full (allocation) | Normal routine cleaning | If too frequent, cut garbage or enlarge young area |
| Humongous allocation | A huge object was created | Increase region size, reduce batch sizes |
| Concurrent cycle started | Old area reached the marking threshold | Normal in moderation; tune the threshold if too frequent |
| Explicit collection requested | Code or a library asked for a collection | Make explicit requests run concurrently, find the caller |
| Metadata threshold | Class metadata space filled up | Set a larger initial metaspace |
| Full collection after failure | G1 fell behind or ran out of space | Larger heap, earlier marking, less garbage |
Patterns over time
One line rarely tells the story. Zoom out and look for shapes. A steady sawtooth of the heap rising and dropping is normal. A sawtooth whose lowest points climb steadily is a growing live set, possibly a leak or ever more partitions. A sudden vertical jump followed by a long full collection is often a burst of humongous allocations or a coordinator loading a large state. Regular spikes at exact intervals, such as every hour, point at scheduled jobs on the broker or on its clients.
Example: the rising floor
A team reviews a week of GC logs and sees that after each collection, the heap used drops to about 2.1 gigabytes on Monday, 2.4 on Wednesday and 2.9 on Friday. The floor is rising by about 0.4 gigabytes every two days. Correlating with the change log shows that a new microservice platform had been creating dozens of new topics every day. The live data was growing with the partition count. The fix was a governance policy on partitions plus a modest heap increase, not any exotic collector setting.
A good answer to “How do you tell if a long stall is a GC problem?” is: compare GC log pause times with the stall, check the user, system and real times, look for safepoint records that are not collections and compare with disk and processor metrics for the same seconds.
A log line read out loud
Since logs are hard to read at first, here is how an engineer would narrate one entry, without showing the raw text. “At 14:02:31 on Tuesday, a young collection began because the young area was full. Before the collection the heap held 4.6 gigabytes out of a maximum of 6. After it, the heap held 2.3 gigabytes. The collector’s threads worked for a combined 60 milliseconds of processor time, the operating system spent 4 milliseconds on its behalf, and the whole pause lasted 22 milliseconds on the clock.” That is a healthy line: the young area was cleaned, about half the heap was freed, the live set is 2.3 gigabytes, and the pause is close to a 20 millisecond target.
Now imagine the next entry says: “At 14:07:10, a full collection began after an allocation failure. Before it, the heap held 5.9 out of 6 gigabytes. After it, the heap held 3.8 gigabytes. The pause lasted 6.4 seconds.” This is a warning. The heap was nearly full, a full collection was needed, and the live set of 3.8 gigabytes is larger than it was earlier in the day. Such a pair of entries tells you the live set is growing and that marking started too late or the heap is too small, so the next step is to look at what changed between the two.
Finally, picture an entry where the clock time is 4.1 seconds, the collector’s own processor time is 80 milliseconds and the operating system time is 20 milliseconds. Nearly all of the time was spent neither computing nor in the kernel on the collector’s behalf. The threads were waiting, most likely for a disk, for a starved processor or for something that swapped. Reading the entry this way, you know at once that the fix is outside the JVM settings.
10When It Is Not the Collector: Safepoints, the OS and the Cloud
Many “GC pauses” turn out to be something else that freezes the machine. A good diagnosis rules these out.
Safepoints: the JVM’s parking spots
A safepoint is a moment when the JVM brings all application threads to a stop so it can do certain jobs safely. Garbage collection is the most common job, but not the only one. Others include revoking special locks, recompiling code, dumping threads and applying certain tools’ requests. Each time, the JVM must wait until every thread has reached a safe spot, and a thread running a long loop without safe spots can hold everyone else up. The JVM can log the total time that application threads were stopped and the time spent waiting for them to reach the safepoint. If your clients see a stall and the GC log shows nothing, safepoint logging is the next place to look.
Memory pressure and swapping
If the operating system runs short of memory, it can move parts of a process to the swap area on disk. When the garbage collector then touches those parts, they must be read back, and a pause that should take 20 milliseconds can take seconds. Kafka guidance is to keep swapping to an absolute minimum, either by turning swap off or by setting the swappiness value very low. A high system time in the GC log is the classic clue.
Transparent huge pages
Linux can group small memory pages into big ones automatically, a feature called transparent huge pages. In theory it saves work. In practice, defragmenting memory to build these big pages can cause unpredictable stalls, again visible as high system time. Many database and messaging systems recommend disabling it or setting it to a mode that avoids on-demand compaction. Check what your operating system image uses by default.
Processor starvation: steal and throttling
Two situations can stop the collector from getting the processor time it needs. On virtual machines, a busy neighbor can take processor time that was promised to you, which shows up as a steal counter. On burstable cloud instance types that give a baseline of processor use and let you spend saved credits, running out of credits reduces you to the baseline and everything slows down together. In containers, a processor quota can cause the kernel to pause your threads for the rest of each accounting period once they have used their share, which looks like random freezes of tens or hundreds of milliseconds. The collector uses many threads at once, so it hits the quota especially fast.
Do not run production Kafka brokers on burstable instance types or with tight container processor limits. The average may look fine, but the moment a busy collection needs many threads at once, throttling turns a 20 millisecond pause into a second or more.
Disk and file system stalls
Kafka writes to disk in the background, and the operating system flushes dirty pages when it chooses. If the disk becomes slow, for example because of a storage volume that has used up its burst allowance, threads that touch the file system can block. If a thread that is needed to reach a safepoint is blocked on the disk, the whole JVM waits for it. Such stalls appear as long real times with little user or system time. On AWS, check the volume’s throughput and operations limits and its credit or burst state.
| Suspect | Evidence to look for | Typical fix |
|---|---|---|
| Swapping | High system time, swap-in activity, memory nearly full | Disable or minimize swap, reduce heap or other memory use |
| Transparent huge pages | High system time and irregular stalls | Disable on-demand compaction |
| Processor steal | Steal counter above a few percent | Move to dedicated or larger instances |
| Burst credits exhausted | Sudden processor drop to baseline | Use non-burstable instance families |
| Container throttling | Throttled-time counters increasing | Raise or remove the processor limit, size threads accordingly |
| Slow disk | High disk wait, volume at its limit | Faster or larger volumes, spread partitions, throttle heavy readers |
| GC log on slow disk | Real time far above user plus system, right at log writes | Move logs to a fast local device |
| Noisy neighbor process | Other processes using processor or disk at the same time | Isolate the broker, remove co-located jobs |
If a car stutters, the engine is not always the culprit. It could be bad fuel, a clogged filter or a slipping belt. A mechanic checks the whole car before rebuilding the engine. In the same way, check the machine and the cloud around the JVM before rewriting the JVM settings.
11A Step-by-Step Diagnosis Method
Random guessing wastes hours. This method moves from the simplest question to the most detailed, and each step narrows the search.
flowchart TD
A["Symptom: latency spikes, ISR shrinks, timeouts"] --> B["Step 1: fix the time window and the broker or brokers affected"]
B --> C{"Does the GC log show a long pause at that time?"}
C -->|Yes| D{"Is it a full collection?"}
C -->|No| E{"Do safepoint records show a long stop?"}
D -->|Yes| F["Heap too small, live set too big, or marking too late"]
D -->|No| G{"Is real time far above user plus system?"}
G -->|Yes| H["Machine problem: disk, steal, throttling"]
G -->|No| I{"Is system time high?"}
I -->|Yes| J["Swap or huge pages"]
I -->|No| K["Too much garbage or oversized pause target"]
E -->|Yes| L["Non-GC safepoint work or blocked thread"]
E -->|No| M["Not a JVM pause: check disk, network, load, client behavior"]
The steps in detail
Pin down when and where
Write down the exact start and end time of each bad episode and which brokers were affected. Use the same time zone everywhere, since mismatched clocks are a common source of confusion.
Check the GC log for that window
Find any collection whose pause is long enough to explain the symptom. Note its type, cause, duration and heap sizes. If none exists, check safepoint records.
Read the three times
Compare user, system and real times to decide whether the collector was slow or the machine was slow.
Check the heap shape
Look at heap after collection over days. Is it stable, or is the floor rising? Also look at the allocation rate: how fast the heap fills between collections.
Look at the surroundings
Line up processor, memory, swap, disk latency, network and deployments for the same minutes. Compare with other brokers.
Form one hypothesis and test it
Choose the most likely cause, change one thing in a controlled way, on one broker if possible, and measure the effect over enough time to include a busy period.
Record the finding
Write down what you found, how you found it and what fixed it. The next incident will look similar.
Questions that speed up the search
What changed recently?
New clients, TLS turned on, new topics, a version upgrade, a new instance type or a configuration change. Most incidents follow a change.
Did traffic or connections change?
More producers, bigger batches or a spike in consumer groups all raise the allocation rate or the live set.
Is it periodic?
Exact intervals point to scheduled jobs, log cleaners, snapshot tasks or client cron jobs.
One broker or all?
Local versus shared causes need completely different investigations.
Keep a one-page checklist with these steps next to your on-call runbook. Under pressure, people skip steps. A checklist stops the team from jumping to “let us restart it” before collecting any evidence.
A complete worked diagnosis
A team sees complaints: every day around 9:15 in the morning, some producers report timeouts for about ten seconds, and dashboards show a burst of in-sync set shrinks on broker 4. Follow the method step by step.
Step one fixes the window: 9:14 to 9:16, broker 4 only, on weekdays. Only one broker is involved, so the cause is probably local. Step two opens the GC log for broker 4. At 9:15 there is one long collection, a full collection, lasting eleven seconds. Step three reads the times: user time is large, system time is small and real time is close to user time divided by the number of threads. The collector was doing honest work, so this is a heap problem and not a machine problem. Step four looks at the heap shape: after collection the floor sits at 5 of 6 gigabytes, much higher than on the other brokers, and the log shows the humongous allocation cause a few seconds before the full collection.
Step five looks at the surroundings and finds that broker 4 leads the partitions of a new reporting topic whose producers send batches of many megabytes when the business day starts. Those batches become humongous objects that fill the old area. Step six forms the hypothesis and tests it in one controlled change: increase the G1 region size on broker 4 only, cap the producers’ batch size and move half the reporting partitions’ leadership to other brokers. The full collection disappears the next morning. Step seven records the finding: batch-size limits are now part of the onboarding checklist for new topics. The whole diagnosis followed evidence at each step and needed no guessing.
12Pattern Library: Common Root Causes and Their Fixes
Most real incidents fit one of a handful of patterns. Learn them, and the diagnosis often takes minutes.
Pattern 1: heap too small for the workload
Signs: collections happen many times per second, the heap after collection is close to the maximum, and occasional full collections occur. Cause: the default heap of one gigabyte, or a heap sized for an older, smaller workload. Fix: set the starting and maximum heap to the same value, choose a size that keeps the heap after collection below about half of the maximum, and check that the page cache still has plenty of room.
Pattern 2: humongous allocation storms
Signs: GC log entries with the humongous allocation cause, extra concurrent cycles and rising old area between mixed collections. Cause: large client batches, big fetch sizes or large messages creating buffers bigger than half a region. Fix: raise the G1 region size, review client batch and fetch settings, and check that message sizes are reasonable.
Pattern 3: encryption and conversion overhead
Signs: collections became more frequent after enabling encrypted listeners or after a group of old clients started connecting. Cause: loss of the direct file-to-network path and extra copies for conversion or encryption. Fix: upgrade old clients so that no format conversion is needed, size the heap for encrypted traffic and consider more brokers to lower the traffic per broker.
Pattern 4: too many partitions per broker
Signs: heap after collection climbs steadily as topics are added, and restarts and recovery take longer. Cause: the live set grows with partition count, since each replica has objects for its logs, indexes and producer state. Fix: govern partition counts, delete unused topics and add brokers. See the companion guide on partition limits per broker for the sizing method.
Pattern 5: consumer group and offset load
Signs: a large jump in heap and a long pause right after a broker restart or after leadership of the offsets topic moves. Cause: the group coordinator loads the committed offsets of many groups into memory. Fix: remove dead groups, avoid creating a new group for every short job, keep offset retention sensible and plan restarts in quiet periods.
Pattern 6: explicit collection requests
Signs: GC log entries whose cause is an explicit request, occurring at regular intervals, often when direct buffer memory approaches its limit. Cause: library code, or the JVM’s own direct-buffer management, asking for a full collection. Fix: tell the JVM to run explicit requests as concurrent cycles instead of full stop-the-world ones, and investigate what requests them.
Pattern 7: marking starts too late
Signs: the old area fills past the threshold, the log shows a full collection or an evacuation failure, and the heap was not extreme. Cause: G1 began its marking cycle too late to finish before space ran out. Fix: lower the initiating occupancy percentage, so marking begins earlier, and make sure the collector has enough processor threads.
Pattern 8: machine-level freezes
Signs: real time is much larger than user plus system time, or system time dominates, and the pattern is unrelated to load. Cause: swapping, huge page compaction, steal, throttling or slow disks. Fix: repair the environment as in Chapter 10 before touching JVM flags.
Do not copy a long list of JVM flags from an internet post and apply them all at once. When something changes, you will not know which flag helped or hurt. Change one thing at a time and measure.
13Tuning the JVM: Heap, G1 and Java Version
Once you know the cause, these are the JVM-level levers you can pull, in order of how often they matter.
Choose a supported Java version
Newer Java versions bring a faster and better G1, and they fix bugs that older ones still have. Kafka 3.x supports several long-term releases, and Kafka 4.0 requires Java 17 for brokers, controllers and tools. Running an old release just because “it works” often means running with slower collectors and unfixed memory bugs. Always test an upgrade in staging under load, since the pause profile can change in either direction.
Size the heap
Start with the starting and maximum sizes set equal, so the heap never needs to resize during operation. A common range for busy brokers is roughly 4 to 8 gigabytes, and Kafka’s own operations guidance quotes 6 gigabytes for a large production cluster. Then measure the heap after collection under peak load. A good sign is that the live set uses well under half of the heap, leaving room for bursts. If it uses more than that, increase the heap in steps of a gigabyte or two, and confirm that page cache hit rates and disk reads do not suffer.
| Observation | Suggested action |
|---|---|
| Heap after collection over 60 percent of maximum | Increase the heap or reduce live data such as partitions |
| Heap after collection under 25 percent and pauses are short | Heap may be larger than necessary; consider returning memory to the page cache |
| Young collections more often than once per second | Look for high allocation rate; consider a larger young area or fewer allocations |
| Pauses regularly above the pause target | Check live set, threads and machine issues before lowering the target |
| Any full collection in normal operation | Treat as a defect and find its cause |
Set G1 with measured intent
The recommended starting point in the Kafka documentation is to use G1 with a modest pause target, an earlier marking threshold than the default and a larger region size. Use those as a baseline, then adjust based on your logs. If mixed collections come too late and full collections occur, lower the marking threshold. If marking runs almost constantly and wastes processor time, raise it a little. If humongous allocations are frequent, increase the region size. If pauses cluster around the target but clients still complain, the target may be simply too high for their deadlines, and the answer might be to reduce the amount of work per collection by reducing the live set.
Pre-touching memory
By default, the operating system gives the JVM memory pages as they are first used, and that first touch can cause small stalls as the heap grows into new memory. A setting called always pre-touch makes the JVM touch all heap pages at startup, which slows the start a little and removes those stalls later. It is worth testing on brokers with large heaps, especially in virtualized environments.
Direct buffers and explicit collections
Java networking code uses direct buffers that live outside the heap. When the total of direct memory approaches its limit, the JVM may request a collection to free unused buffers. If that request causes a full stop-the-world collection, you get a mysterious full pause with a modest heap. Setting the JVM to treat explicit requests as concurrent cycles avoids this and is part of the Kafka documentation’s sample settings.
What about other collectors?
Newer low-pause collectors, such as ZGC and Shenandoah, aim for pauses in the range of a millisecond or so, regardless of heap size. Some teams have experimented with them for brokers. The trade-offs are a higher processor and memory overhead and far less shared operating experience with Kafka than G1. If you are curious, test in a staging cluster with production-like traffic, and compare the whole picture including throughput and page cache use, not just pause length. For most clusters, a well-tuned G1 on a current Java release is the right default.
Kafka’s authors and many operators have run G1 for years. Being conservative here is not laziness. Predictable behavior under failure conditions is worth more than the last few milliseconds of pause time.
How to run a safe tuning experiment
Write the hypothesis
For example: “Lowering the marking threshold will remove full collections during the morning peak.”
Choose one broker as the canary
Pick a broker with a typical load and confirm that the others make a good comparison group.
Change one setting
Apply it with a normal rolling restart process and note the exact time.
Observe a full traffic cycle
At least one busy period, and ideally a full day and night, so daily patterns are included.
Compare with the control brokers
Check pause length, frequency, full collections, processor use and client latency, not just one number.
Decide and document
Roll out, adjust or roll back, and write the result down with the data.
14Reducing GC Pressure from the Kafka Side
Often the best “GC fix” is not a JVM flag. It is making the broker create less garbage and hold less data in the first place.
Control the live set
Govern partition counts
Fewer partition replicas per broker means fewer long-lived log objects, less metadata and faster recovery.
Clean up consumer groups
Delete unused groups, set sensible offset retention and avoid creating a new group for every short-lived job.
Limit connection storms
Set connection limits per client address and reuse producer and consumer instances instead of creating many.
Delete unused topics
Dead topics still cost heap and metadata. A regular clean-up reduces the live set for free.
Control the allocation rate
Upgrade clients
Modern clients use current message formats, so the broker does not need conversions and can use faster paths.
Reasonable batch and fetch sizes
Very large batches create huge buffers. Choose sizes that balance efficiency and memory, and test their effects.
Let producers compress
Keep the broker from recompressing by leaving the compression setting at the producer’s choice, and use compression that clients can decode efficiently.
Avoid pathological readers
Consumers that fetch tiny amounts in a tight loop, or that read far behind the end of the log, multiply requests and allocations.
Protect the broker with quotas
Client quotas cap how many bytes per second, and what share of request processing time, a client may use. A single runaway client, such as a job that reads a year of history at maximum speed, can raise the allocation rate for everyone on that broker. Quotas turn a flood into a manageable stream and are one of the cheapest tools for smoothing garbage collection behavior. Set them per service account and review them regularly.
Spread the load evenly
If one broker leads far more busy partitions than the others, it produces far more garbage. Keep leaders balanced using preferred leader election, keep partitions balanced across brokers, and consider tooling that watches traffic and rebalances automatically. A single hot broker is a common reason why GC pauses seem to follow one machine around.
Example: the nightly analytics job
Every night at two o’clock, one broker in a cluster shows a two-second stall. The team discovers that a data warehouse job starts then and reads a week of old data from thousands of partitions at once, forcing the broker to read old segments from disk and to allocate large fetch responses. Adding a bandwidth quota for that job’s service account, and spreading its start times, reduces the stall to a fraction of a second, with no JVM change at all.
15Special Cases: Controllers, ZooKeeper, Managed Services and Containers
The advice so far applies to ordinary brokers. Other components and environments change what you can see and what you can fix.
KRaft controllers
In modern Kafka clusters, a small group of controller nodes holds the cluster’s metadata. Controller processes are also Java programs with a heap and a garbage collector. A long pause on the active controller delays every cluster-wide decision: elections, topic creation and broker registrations. Since the controller stores the entire metadata in memory, its heap should be sized for the total partition count. Controllers should run on dedicated, well-provisioned machines in large clusters, and their GC logs need the same monitoring as brokers’.
ZooKeeper in older clusters
If you still use ZooKeeper, remember that it is a separate Java program that also suffers from pauses. A ZooKeeper pause can cause broker sessions to expire even when brokers themselves are healthy. Give ZooKeeper a dedicated machine, a modest heap that is sized so it never swaps, and a fast dedicated disk for its transaction log. When brokers report lost ZooKeeper sessions, check both sides of the connection.
Amazon MSK and other managed services
On a managed service such as Amazon MSK, you do not control the JVM flags, the operating system or the instance internals. What you can do is watch the metrics the service publishes, such as heap memory in use after garbage collection, processor use by user and system, memory and disk metrics, and lag or under-replicated partition counts. Your levers are the workload and the size of the brokers. Reduce partitions and garbage from clients, add brokers to spread load, move to a larger broker size and, when the evidence points at the platform rather than your usage, open a support case with exact times and broker identifiers. Check the current documentation to see which metrics exist for your broker type and monitoring level.
Containers and Kubernetes
When brokers run in containers, memory and processor limits shape the JVM’s behavior. Modern Java versions read the container limits and size their thread pools and default heap accordingly, but you should still set the heap explicitly. Leave enough memory outside the heap for direct buffers, metaspace, thread stacks and the page cache, because the container’s memory limit covers all of them. Avoid tight processor limits for the reasons in Chapter 10, and set requests equal to limits so that the platform treats the broker as a guaranteed workload. Use local or high-performance storage, and do not put GC logs on a shared network volume.
| Environment | What you control | What you cannot control | Best diagnostic approach |
|---|---|---|---|
| Self-managed on virtual machines | JVM flags, operating system, instance choice | Neighbors and hypervisor behavior | Full GC log analysis plus steal and disk metrics |
| Containers on Kubernetes | Heap, resource requests and limits | Node-level contention, shared storage behavior | GC logs plus throttling counters |
| Amazon MSK | Workload, broker size and count, cluster configuration where allowed | JVM flags and operating system | Service metrics and workload reduction, support case if needed |
| Bare metal | Everything | Hardware faults | Complete evidence collection and careful tuning |
Whatever the environment, keep a small set of “known good” baselines for GC behavior: normal pause length, normal collection frequency, normal heap after collection. A baseline turns “this looks bad” into “this is twice the normal level.”
16Incident Response: What to Do While It Is Happening
During an incident the goal is to protect users first and to preserve evidence second. Fixing the root cause comes later.
Confirm the scope
Is it one broker or many? Check under-replicated and offline partitions, request latency and client errors.
Capture evidence quickly
Copy the current GC log, note the time, and if a recording is running save it. Take a thread dump only if the broker can tolerate it.
Reduce the load on the struggling broker
Move leaders away from it using preferred leader election or a controlled reassignment, and apply or tighten quotas on heavy clients.
Stabilize clients
If a retry storm is under way, ask affected teams to slow down retries or scale down non-critical consumers for a short while.
Restart only as a last resort
A restart clears the heap and buys time, but it also empties the page cache, forces log recovery and destroys clues. Do it one broker at a time, and only after collecting evidence.
Verify recovery
Wait until replicas are back in sync and leaders are balanced before declaring the incident over.
Write the review
Record the timeline, evidence, root cause and follow-up actions, with owners and dates.
Restarting every broker at once, or raising all timeouts to a huge value, or applying a random new set of JVM flags during the incident, can turn a partial problem into a total one. Move slowly, one change at a time, and watch the effect.
Temporary safety nets
While you work on the root cause, a few settings buy safety. A somewhat longer session timeout for brokers can prevent needless fencing during rare pauses, though it delays the detection of real failures. Allowing producers to retry with sensible back-off protects against short errors. A less strict minimum for in-sync replicas can restore write availability at the cost of some durability, and that trade should be a deliberate business decision, never a silent default. Document each safety net and schedule its removal, so that temporary changes do not become permanent hidden risks.
Communication during a pause incident
Technical work is only half of an incident. Assign one person to keep a running timeline in a shared document, one to talk to affected teams and one to do the hands-on investigation, so that the person analyzing logs is not interrupted every few minutes. Tell client teams plainly what they should do: keep retries with back-off, avoid restarting their applications all at once and expect a short period of catch-up. Clear, calm messages prevent the well-meaning but harmful reactions that make garbage collection worse, such as a hundred applications restarting and reconnecting at the same time.
17Prevention: Monitoring, Alerting and Testing
The best pause is the one you find in a test, before customers do.
What to alert on
| Signal | Why it matters | Suggested alert |
|---|---|---|
| Longest pause in the last few minutes | Compare with the shortest timeout in your system | Warn at a fraction of that timeout, page when close to it |
| Share of time spent in collection | Shows a broker drowning in garbage | Warn if it stays above a few percent |
| Full collection count | Should be zero in healthy operation | Alert on any occurrence |
| Heap after collection trend | Reveals a growing live set | Warn on steady growth over days |
| Young collection frequency | Reveals a rising allocation rate | Warn on sudden change from baseline |
| Under-replicated partitions and in-sync set shrinks | Symptoms downstream of pauses | Alert on sustained or repeated bursts |
| Processor steal, throttling and swap activity | Environmental causes of stalls | Warn on any non-zero swap and on steal or throttling above small thresholds |
Test before you ship
Before changing the JVM version, the flags, the heap size, the client mix or turning on encryption, run a load test on a staging cluster that has the same machine type and a realistic number of partitions. Record GC logs during the test and compare them with your baseline. Include a failure test: stop a broker, then bring it back, and watch the collection behavior during catch-up, because recovery often has the highest allocation rate of all.
Game days
Schedule regular exercises in which the team practices the response in Chapter 16 on a test cluster. Inject a stall using a controlled tool, for example pausing the process for several seconds, and see how the cluster behaves: which alarms fire, how long elections take and what the clients see. The practice reveals gaps in dashboards and runbooks while the stakes are low.
Feed findings back into design
Each incident should improve something structural. If pauses were caused by partition growth, add partition governance. If they were caused by an old client, add a client version policy. If they were caused by a noisy job, add quotas. If they were caused by the environment, add a platform check. Over time, the number of surprise pauses falls, and the ones that remain are easy to explain.
flowchart LR
CHG["Proposed change: JVM, clients, encryption, size"] --> STG["Staging load test with GC logs"]
STG --> CMP{"Compare with baseline"}
CMP -->|Worse| FIX["Adjust plan"]
FIX --> STG
CMP -->|Same or better| CAN["Canary on one broker"]
CAN --> OBS["Watch pauses, ISR, latency for a full cycle"]
OBS -->|Healthy| ROLL["Roll out gradually"]
OBS -->|Unhealthy| BACK["Roll back and record findings"]
18Trade-offs, Patterns, Anti-patterns and Best Practices
Every choice about memory and collectors trades one good thing for another. Make the trade on purpose.
Benefits of a larger heap
- More room for bursts of garbage and for growth in partitions and groups.
- Fewer collections per minute.
- Lower risk of running out of space during marking.
Costs of a larger heap
- Less memory for the page cache, so more disk reads.
- Longer collections when the live set is large.
- Longer time to write a heap dump or to recover from a full collection.
- More memory to pre-touch and manage.
Benefits of a very low pause target
- Smaller worst-case stalls in normal operation.
- Better fit for strict latency deadlines.
Costs of a very low pause target
- More frequent collections and more processor use.
- Risk of falling behind and entering full collections.
- Harder to reach with a large live set.
Patterns that work
Always log GC
Keep rolling GC logs on a fast local disk, with timestamps and days of history.
Baseline first
Know your normal pause, frequency and heap-after-collection before an incident.
Change one thing
Alter one setting at a time, on one broker, and measure over a full traffic cycle.
Reduce the live set
Govern partitions, groups and topics so the collector has less to examine.
Quotas as shock absorbers
Cap heavy clients so their bursts cannot flood the heap.
Dedicated capacity
Use non-burstable machines with predictable processor and disk performance.
What people do
They give brokers a giant heap, such as half of the machine’s memory, “so that garbage collection never runs out of room.”
Why it hurts
The page cache shrinks, so reads hit the disk. The collector has far more memory to scan, so pauses lengthen, and a full collection on such a heap can take tens of seconds, long enough to lose the broker from the cluster.
What to do instead
Size the heap from measured live data, keep it moderate and let the operating system use the rest as cache.
What people do
They react to pause incidents by raising every timeout and adding a long list of JVM flags collected from many sources.
Why it hurts
Real failures are detected late, and nobody knows which flag helps or harms. The cluster becomes fragile and hard to reason about.
What to do instead
Find the cause with evidence, apply the smallest targeted fix and remove temporary safety nets when the fix is proven.
Common mistakes checklist
- Leaving the default one-gigabyte heap on a production broker.
- Judging memory health by total machine memory instead of heap after collection.
- Ignoring GC logs until an incident, then discovering they were never enabled or were overwritten.
- Assuming every stall is a collection without checking safepoints and the machine.
- Running on burstable instances or with tight container processor limits.
- Leaving swap and transparent huge page defaults unexamined.
- Taking a heap dump from a struggling broker still in service.
- Upgrading Java or enabling encryption without a load test.
- Letting partitions, groups and topics grow without limits.
Best practices in one place
Use a current supported Java version with G1. Set equal starting and maximum heap sizes that fit the measured live set. Keep GC logs, JMX metrics and a rolling flight recording. Learn your baseline. Read the three GC times to separate collector problems from machine problems. Keep the live set small through governance, and the allocation rate small through modern clients and quotas. Change one thing at a time, and write down what you learn.
19Real-World Usage Patterns
The same story plays out in companies of every size, with the same handful of endings.
The busy shared cluster with tuned G1
Kafka’s own operations documentation describes one of LinkedIn’s busiest clusters as an example: dozens of brokers, tens of thousands of partitions and hundreds of thousands of messages per second, run with a modest heap of a few gigabytes and an aggressively tuned G1. It reports that the 90th percentile pause was around twenty milliseconds, with less than one young collection per second. The lesson is not to copy the numbers, but to see that a small heap with a low pause target, measured and tuned, can serve enormous traffic.
The compliance-driven encryption rollout
A financial company enables encrypted connections on all listeners. GC pauses lengthen during the first busy week, and the team sees more frequent collections and occasional latency spikes on the heaviest brokers. They plan the fix from evidence: a larger heap after measuring the live set, a check on batch sizes, an upgrade of the remaining old clients and two more brokers to lower the traffic per broker. The next quarter passes without stalls.
The runaway consumer
A team launches a backfill job that reads months of history at full speed. One broker begins to show multi-second stalls every few minutes as it serves large fetches for cold data. The platform engineers notice that only one broker is affected, that the job’s service account dominates its traffic and that GC logs show humongous allocations. They apply a bandwidth quota to the job, and the stalls disappear while the job still completes overnight.
The cloud instance surprise
A start-up runs its brokers on burstable virtual machines to save money. For months the graphs look fine. During a traffic surge the machines exhaust their processor credits, and every collection suddenly takes several seconds because the collector threads are starved. Brokers drop out of the cluster one after another. The fix is to move to a steady instance family, and the lesson is that cost savings on the platform layer can create outages in the application layer.
Managed Kafka on AWS
A team on Amazon MSK sees heap-after-collection metrics rising over weeks and periodic spikes in request latency on two brokers. They cannot change the JVM, so they act on the workload: they delete hundreds of unused topics, retire idle consumer groups, upgrade old clients and move to a larger broker size. The heap metric flattens and the spikes vanish. They keep the CloudWatch graphs on their main dashboard as an early-warning signal.
Lessons that repeat
Teams that handle pauses well share four habits. They always have GC logs and metrics before a problem starts. They check the machine before blaming the collector. They reduce garbage and live data rather than only tuning flags. And they treat every incident as a chance to improve monitoring and governance. The technology is the same for everyone. The habits are what differ.
20Frequently Asked Questions
21Summary and Key Takeaways
What to remember
- A pause looks like a crash to the cluster: a frozen broker misses heartbeats and fetches, so long pauses lead to fencing, elections, out-of-sync replicas and client errors.
- Kafka’s heap is small on purpose: data lives on disk and in the page cache, so keep the heap moderate and give the rest of the memory to the operating system.
- Two things drive GC pain: how fast garbage is created and how much long-lived data exists. Fix the cause by reducing one of them, not only by adding flags.
- Collect evidence before you need it: keep GC logs on a fast disk, JMX metrics on a dashboard and a rolling flight recording, and learn your baseline.
- Read the log with the three times: user, system and real time separate a collector problem from a swapping, throttling, steal or disk problem.
- Follow the method: pin down the time, check the log, read the times, examine the heap shape, look at the surroundings, test one change and record the result.
- Prevent, do not just react: govern partitions and groups, upgrade clients, use quotas, choose steady instances, test changes under load and rehearse the response.