Kafka brokers are JVM processes, and like any JVM process, they need careful tuning of their heap size and garbage collection strategy to perform optimally under production load.

Let’s see a Kafka broker in action, processing a steady stream of messages.

# Simulate a Kafka broker's JVM metrics
# (This is illustrative, actual metrics come from JMX or Prometheus)

# Heap Usage (MB)
# Total: 8GB (8192MB)
# Used: 6GB (6144MB)
# Committed: 7GB (7168MB)

# GC Activity (last minute)
# Young Gen GC: 15 collections, 120ms total time
# Old Gen GC: 2 collections, 500ms total time
# Total GC time: 620ms

# Throughput: 99.99%
# Pause Times: Max 150ms, Average 70ms

This shows a broker with a significant portion of its heap in use, and a noticeable amount of time spent in garbage collection, particularly in the old generation. The goal of tuning is to keep heap usage predictable, minimize GC pauses, and maximize application throughput.

The core problem Kafka solves is reliable, scalable, and fault-tolerant streaming of data. To do this efficiently, it uses a JVM. The JVM manages memory for Kafka’s internal data structures, caches, and thread pools. When this memory management becomes a bottleneck, it directly impacts Kafka’s ability to produce and consume messages, leading to increased latency, dropped requests, and even broker instability.

Kafka brokers require a substantial heap for several reasons:

  • Message Buffers: Kafka reads messages from producers and writes them to disk. These operations often involve buffering data in memory.
  • Page Cache: Kafka heavily relies on the operating system’s page cache for performance. While not directly part of the JVM heap, the JVM heap needs to be sized appropriately so it doesn’t starve the OS cache.
  • Internal Data Structures: Kafka maintains metadata, topic configurations, and internal state within its JVM process.
  • Network Buffers: Managing connections and data transfer requires memory for network buffers.

The KAFKA_HEAP_OPTS environment variable is your primary lever for controlling the JVM heap. It’s typically set in the kafka-server-start.sh script or within systemd service files.

# Example: Setting heap options for Kafka
export KAFKA_HEAP_OPTS="-Xms8g -Xmx8g"

Here, -Xms8g sets the initial heap size to 8 gigabytes, and -Xmx8g sets the maximum heap size to 8 gigabytes. For production, it’s crucial to set -Xms and -Xmx to the same value to prevent the JVM from resizing the heap dynamically, which can cause disruptive pauses. A common recommendation is to allocate between 4GB and 16GB for production brokers, depending on the workload and available system memory. Avoid allocating more than 50% of system RAM to the Kafka heap, reserving the rest for the OS page cache.

Garbage collection (GC) is the process by which the JVM reclaims memory that is no longer in use. Different GC algorithms have different trade-offs between throughput (how much work can be done) and latency (how long the application pauses). For Kafka, low-latency GC is paramount.

The default GC in modern JVMs (like OpenJDK 11+) is G1GC (-XX:+UseG1GC). G1GC is generally a good starting point for Kafka as it aims for predictable pause times.

# Example: Explicitly setting G1GC and tuning its parameters
export KAFKA_HEAP_OPTS="-Xms8g -Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:G1HeapRegionSize=16m -XX:InitiatingHeapOccupancyPercent=35"
  • -XX:+UseG1GC: Enables the G1 Garbage Collector.
  • -XX:MaxGCPauseMillis=100: This is a target for the maximum GC pause time. G1GC will try to meet this target, but it’s not a hard guarantee. Setting this too low can lead to more frequent GC cycles.
  • -XX:G1HeapRegionSize=16m: G1 divides the heap into regions. This setting defines the size of these regions. A typical value is between 1MB and 32MB. The JVM calculates an optimal size if not specified, but explicit setting can sometimes help.
  • -XX:InitiatingHeapOccupancyPercent=35: This percentage determines when G1 starts its concurrent marking cycle. A lower value means it starts earlier, potentially preventing the heap from filling up too much, but can lead to more frequent concurrent cycles. The default is 45%.

Understanding the heap’s generation model is key. The Young Generation (Eden and Survivor spaces) is where new objects are allocated. Most objects die young. The Old Generation (Tenured space) holds long-lived objects. Frequent, short pauses often come from Young Generation GCs, while infrequent but potentially longer pauses come from Old Generation GCs (Full GCs). G1GC tries to manage both efficiently.

If you’re seeing frequent, long pauses, it’s often a sign that your heap is too small, or you have too many short-lived objects being created, or your GC tuning is too aggressive. Conversely, if GC is consuming too much CPU, your heap might be too large, or the GC algorithm isn’t suited for your workload.

A common issue is underestimating the memory needs for high-throughput topics or topics with large message sizes. If your broker is constantly garbage collecting or experiencing high latency, the first step is to increase the heap size, ensuring you leave enough room for the OS page cache. For example, if you have 32GB of RAM, allocating 12GB (-Xms12g -Xmx12g) is a reasonable starting point.

Another critical setting is KAFKA_LOG_DIRS. While not directly a JVM setting, the disk I/O performance is intimately tied to memory. If disk I/O is slow, Kafka will naturally try to buffer more in memory, increasing heap pressure. Ensure your storage is fast (SSDs are highly recommended) and that your KAFKA_LOG_DIRS are on separate, high-performance disks.

If you’re migrating from an older JVM or GC algorithm (like ParallelGC), switching to G1GC and tuning MaxGCPauseMillis can significantly improve latency. Remember to monitor jvm.gc.pause metrics to observe the impact of your changes.

If G1GC is still causing unacceptable pauses, especially during peak loads, you might explore other GC algorithms like ZGC or Shenandoah (available in newer JDKs) which offer even lower pause times at the cost of potentially higher CPU usage.

The most subtle but impactful tuning often involves the interaction between Kafka’s internal buffering and the JVM heap. Kafka’s message.max.bytes and replica.fetch.max.bytes settings can lead to large individual messages being processed, which in turn can increase temporary memory pressure within the JVM. If message.max.bytes is set to 10MB, and you have many such messages being processed concurrently, the JVM heap will need to accommodate these buffers.

The next problem you’ll encounter after optimizing heap and GC is likely related to network I/O saturation or disk I/O bottlenecks, which will manifest as increased request latency and producer/consumer timeouts.

Want structured learning?

Take the full Kafka course →