the 50-percent rule, the 32 GB limit and the Lucene file cache
JVM heap tuning in Elasticsearch is not a matter of "more heap is always better", it is a balance between the JVM heap for Java objects and the operating system file cache that Lucene uses for segment access. Setting the heap too large, or without regard for the 32 gigabyte compressed oops boundary, wastes performance and risks long garbage collection pauses right when load is highest.
Table of Contents
- 1. Why JVM heap tuning should not be guesswork
- 2. The 50-percent rule: heap versus operating system
- 3. The 32 GB compressed oops limit
- 4. Off-heap: the Lucene file cache
- 5. Configuring heap in practice
- 6. Garbage collector: configuring G1GC correctly
- 7. Detecting and avoiding GC pauses
- 8. Circuit breakers as protection against heap overflow
- 9. Keeping heap usage in view long term
- 10. Summary
- 11. FAQ
1. Why JVM heap tuning should not be guesswork
Elasticsearch runs on the JVM and inherits all the quirks of Java memory management, including garbage collection. The most common mistake in JVM heap tuning is assuming a bigger heap is automatically better: more heap means more room for objects, but also longer garbage collection cycles, because the collector has to search more memory to find objects no longer referenced. Past a certain size the effect flips and the cluster slows down through GC pauses instead of speeding up.
A second, equally common misunderstanding: Elasticsearch does not need all available RAM as heap. Lucene, the search index layer underneath, accesses the operating system file cache directly through memory-mapped files, without burdening the JVM heap. JVM heap tuning therefore means deliberately splitting memory between heap and file cache, instead of reflexively assigning all RAM to the heap.
The two central rules of thumb for JVM heap tuning, covered in detail in this article, are: never allocate more than fifty percent of available RAM as heap, and never go above the roughly thirty two gigabyte boundary where the JVM optimization compressed oops stops applying. Together, both rules prevent the most common misconfigurations in production clusters.
2. The 50-percent rule: heap versus operating system
The central rule of thumb for JVM heap tuning is to assign at most fifty percent of the RAM available on a data node to the JVM heap. The rest stays with the operating system for the file cache that holds Lucene segment files, as well as for other processes and the network stack. On a node with sixty four gigabytes of RAM, that means a heap of at most thirty two gigabytes, not more.
This split feels counterintuitive at first, because unused heap memory seems wasted. In practice, the file cache is at least as important for search read speed as the heap: Lucene accesses index segments via mmap, and the more of that sits in the operating system cache, the less disk I/O a search request needs. An oversized heap at the expense of the file cache paradoxically leads to slower search requests, even though seemingly more memory is available.
3. The 32 GB compressed oops limit
The JVM normally references Java objects through 64-bit pointers. For heaps under roughly thirty two gigabytes, the JVM automatically enables compressed oops, an optimization that compresses object references to 32 bits, saving memory and improving CPU cache efficiency. Once the heap crosses this threshold, the JVM automatically switches to uncompressed 64-bit pointers, which increases effective memory demand per object.
The paradoxical result: a forty gigabyte heap can in practice provide less usable memory for objects than a thirty one gigabyte heap with compressed oops enabled, because the pointer overhead eats up the apparent size gain. For JVM heap tuning this means: the zone between thirty two and roughly forty eight gigabytes of heap is practically always a worse choice than a heap just under the threshold. The exact boundary varies slightly by JVM version but typically sits between thirty one and thirty two gigabytes.
# jvm.options.d/heap.options
# Rule 1: never exceed roughly 32 GB, compressed oops get disabled above that
# Rule 2: never exceed 50 percent of available system RAM
# Example for a data node with 64 GB total RAM
-Xms31g
-Xmx31g
# Xms and Xmx must always match, prevents heap resizing pauses at runtime
4. Off-heap: the Lucene file cache
The Lucene file cache is not a separately configurable cache in the Elasticsearch sense, it is simply the operating system page cache, which automatically holds recently read files in free RAM. Since Lucene index segments are read through memory-mapped files, repeated access to the same segments benefits directly from the operating system cache, without any explicit configuration inside Elasticsearch itself.
For JVM heap tuning, a clear consequence follows: the RAM not assigned to the heap is not wasted memory, it is actively used cache for the most common access path in a search system, namely reading index data. A data node with sixteen gigabytes of RAM and eight gigabytes of heap effectively has eight gigabytes available for the file cache, a node with sixty four gigabytes of RAM and thirty two gigabytes of heap correspondingly has significantly more cache capacity for the same relative heap share.
5. Configuring heap in practice
In modern Elasticsearch versions, heap is configured through files in the config/jvm.options.d/ directory instead of editing the central jvm.options file directly. This eases automated deployment, since custom heap settings survive Elasticsearch updates. Xms and Xmx, the minimum and maximum heap size, should always be set identically for JVM heap tuning, to prevent the JVM from dynamically resizing the heap at runtime, which itself causes pauses.
Alternatively, since newer versions, the ES_JAVA_OPTS environment variable or Elasticsearch's automatic heap detection can be used, which by default picks fifty percent of available RAM up to the compressed oops boundary. This automatic detection is a good starting point but does not replace a deliberate review for production data nodes with specific workload requirements.
# Alternative: override heap via environment variable at container start
# Useful for Docker or Kubernetes deployments with per-node RAM sizes
ES_JAVA_OPTS="-Xms31g -Xmx31g"
# Elasticsearch also ships an automatic heap sizing script that picks
# roughly 50 percent of available RAM up to the compressed oops limit,
# a reasonable default but always worth verifying for production nodes
| Node RAM | Recommended heap | File cache remaining | Compressed oops |
|---|---|---|---|
| 16 GB | 8 GB | 8 GB | active |
| 32 GB | 16 GB | 16 GB | active |
| 64 GB | 31 GB | 33 GB | active |
| 128 GB | 31 GB (not 64 GB) | 97 GB | active |
| 128 GB (wrong) | 64 GB | 64 GB | disabled |
6. Garbage collector: configuring G1GC correctly
Since Elasticsearch 7, G1GC is the default garbage collector and the right choice for the vast majority of workloads, since it produces shorter and more predictable pauses compared to the older CMS collector. G1GC works on a region basis and can be configured for a target maximum pause time via -XX:MaxGCPauseMillis, though this setting should be understood as a goal, not a hard guarantee.
For most production clusters, it is more sensible for JVM heap tuning to keep G1GC's defaults and instead optimize the heap size itself, rather than manually adjusting individual GC parameters. Detailed GC tuning beyond heap size is usually only needed for very specific workload patterns, such as extreme bulk indexing rates, and should be decided based on GC logs, not on suspicion.
# jvm.options.d/gc.options
# G1GC is the default since Elasticsearch 7, usually leave defaults untouched
-XX:+UseG1GC
-XX:G1ReservePercent=25
-XX:InitiatingHeapOccupancyPercent=30
# Enable GC logging to diagnose pause issues before tuning further
-Xlog:gc*,gc+age=trace,safepoint:file=/var/log/elasticsearch/gc.log:utctime,pid,tags:filecount=32,filesize=64m
7. Detecting and avoiding GC pauses
Long GC pauses are particularly dangerous for Elasticsearch, because a node stuck in a stop-the-world pause longer than the configured timeout can be marked unreachable by the cluster. On a data node this triggers shard reallocation, on a master node it can in the worst case cause an unwanted master switch. The log line [gc][young] with unusually high duration in Elasticsearch logs is usually the first visible symptom.
The most common cause of long GC pauses is an overly full heap due to requests that build large amounts of data in memory, such as high-cardinality aggregations or fielddata on text fields. Effective JVM heap tuning therefore means not only choosing the right heap size, but also identifying problematic query patterns that put unnecessary pressure on the heap, before a pure size adjustment masks the actual problem.
8. Circuit breakers as protection against heap overflow
Elasticsearch uses several circuit breakers that actively abort operations before they overflow the heap and trigger an OutOfMemoryError. The indices.breaker.total.limit caps the total heap share reservable for requests, while more specific breakers like fielddata or request protect individual memory areas. A tripped circuit breaker is unpleasant for the affected request, but far better than a crashed node.
Frequently tripping circuit breakers are not a reason to simply raise their limits, they are a signal for genuine JVM heap tuning: either the heap is too small for the actual workload, or certain requests are inefficient and should be optimized, for example by using doc_values instead of fielddata for aggregations and sorting on text fields.
GET _nodes/stats/breaker
// Response excerpt for one data node, tripped_count above zero
// means a breaker actually rejected a request due to heap pressure
{
"nodes": {
"abc123": {
"breakers": {
"request": { "limit_size_in_bytes": 6442450944, "estimated_size_in_bytes": 1288490188, "tripped": 0 },
"fielddata": { "limit_size_in_bytes": 4295098368, "estimated_size_in_bytes": 4290000000, "tripped": 12 },
"parent": { "limit_size_in_bytes": 16106127360, "estimated_size_in_bytes": 8100000000, "tripped": 0 }
}
}
}
}
Mironsoft
Elasticsearch and OpenSearch operations, heap tuning and performance analysis
Keeping long GC pauses and heap errors under control?
We analyze GC logs, identify memory-hungry query patterns and find the right heap configuration for your data nodes, without adopting blanket values.
GC log analysis
Evaluating pause times and checking heap size against actual load
Query optimization
Identifying and replacing memory-hungry aggregations and fielddata usage
Sizing consulting
Determining heap and RAM sizing for new or existing data nodes
9. Keeping heap usage in view long term
A one-time round of JVM heap tuning at cluster setup is not enough, because workloads and data volume change over time. The _nodes/stats/jvm API delivers detailed heap metrics per node, including the breakdown by memory areas such as young and old generation. Ongoing monitoring of these values reveals early on whether the heap runs constantly close to the ceiling, which is a clear signal for adjustment.
As a rule of thumb: heap usage that stays sustained above seventy five percent and does not drop noticeably after every garbage collection cycle indicates too little heap for the actual load. Conversely, sustained low usage under thirty percent shows that memory could be diverted to the file cache. Good JVM heap tuning is an iterative process guided by real metrics, not by a configuration set once and left alone.
GET _cat/nodes?v&h=name,heap.percent,heap.current,heap.max,ram.percent
// heap.percent sustained above 75 percent across GC cycles
// is the clearest signal that heap sizing needs another look
name heap.percent heap.current heap.max ram.percent
data-node-01 78 24.2gb 31gb 91
data-node-02 41 12.7gb 31gb 88
data-node-03 82 25.4gb 31gb 93
10. Summary
Solid JVM heap tuning follows two clear rules: at most fifty percent of available RAM as heap, and never above the roughly thirty two gigabyte compressed oops boundary, because beyond it the effective memory gain is eaten up by larger pointers. The remaining RAM is not wasted memory, it is actively used Lucene file cache that directly speeds up search requests. G1GC as the default collector should in most cases not be manually readjusted.
Circuit breakers protect against heap overflow, but they are a symptom, not a goal: frequently tripping breakers indicate either too tight a heap or inefficient requests. Anyone continuously watching GC logs and heap metrics instead of relying on values set once will catch misconfigurations before they cause node failures or unwanted master switches.
JVM Heap Tuning: the essentials at a glance
50-percent rule
Never assign more than half of available RAM as heap, the rest belongs to the file cache.
32 GB boundary
Compressed oops stop applying above roughly thirty two gigabytes, a heap just under the threshold is usually more efficient.
Xms equals Xmx
Always set minimum and maximum heap size identically, prevents resizing pauses at runtime.
Watch GC logs
Continuously check heap metrics and pause times, rather than never adjusting after a one-time configuration.