Caching in Elasticsearch: a hands-on guide for engineers
Elasticsearch executes millions of queries every day across our production clusters, and we've learned that raw query speed matters far less than understanding what's actually being cached. Caching in Elasticsearch isn't a single feature you flip on—it's a layered system of node query caches, shard request caches, and field data caches, each solving different performance problems. This guide walks through the built-in caches that help, how they work in practice, and the mistakes we made learning when not to disable them.
Understanding the three layers
Before diving into individual cache types, it helps to know they operate at different levels in the query execution stack. The node query cache sits in memory on each node and stores filter results. The shard request cache stores entire aggregation responses at the shard level. The field data cache loads inverted indices into memory for sorting and aggregations on text fields. Each has its own memory budget, eviction policy, and use case. They're not mutually exclusive—they work together, and understanding their boundaries prevents wasted debugging effort.
Node query cache for filters
The node query cache is Elasticsearch's first line of defense for repeated filter clauses. When you run a query with a filter—say, filtering by status or date range—Elasticsearch caches the bitset result on the node where execution happens. The next identical filter on the same node reuses that bitset without re-scanning the index.
This matters because filters are binary decisions. A filter on status: "active" returns a set of matching document IDs; that result is cheap to cache and reuse. Queries, by contrast, produce relevance scores, so they're not cached this way.
Here's a typical scenario: you run a dashboard query filtering by region and date, then a user applies the same filter with different aggregations. The filter result is already cached, so only the aggregation runs fresh.
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "active" } },
{ "range": { "created_at": { "gte": "2024-01-01" } } }
]
}
},
"aggs": {
"by_region": { "terms": { "field": "region" } }
}
}That filter clause is a prime candidate for caching. By default, Elasticsearch allocates 10% of heap to the node query cache (configurable via indices.queries.cache.size). Filters are cached automatically based on cost and frequency. Elasticsearch tracks which filters are expensive to compute and only caches those—a smart heuristic that prevents cache pollution from cheap filters.
Monitor cache hit rates with the node stats API:
GET /_nodes/stats/indices/query_cache?humanLook for hit_count vs. miss_count. A ratio below 5:1 suggests filters aren't being reused much; a ratio above 50:1 indicates healthy cache behavior. If hit rate is low, check whether filter patterns repeat across queries—if not, the cache is working as intended by not wasting memory.
Shard request cache for aggregations
The shard request cache operates one level higher: it caches entire shard responses for read-only requests. This is powerful for dashboards and reports that query static data. When you run the same query across identical data, every shard can return its cached response immediately.
The catch is that caching only works if the data hasn't changed. Elasticsearch invalidates shard request cache entries as soon as a write occurs on that shard. This makes it perfect for time-series data where you query historical windows that don't change, but less useful for hot, frequently-updated indices.
Consider a metrics dashboard querying the last 7 days of data:
{
"query": {
"range": { "timestamp": { "gte": "now-7d" } }
},
"aggs": {
"hourly_counts": {
"date_histogram": {
"field": "timestamp",
"interval": "1h"
}
}
},
"size": 0
}On the first run, each shard computes the histogram and stores it. Users refreshing the dashboard hit the cache for the next several minutes. Once new data lands (pushing beyond 7 days ago), Elasticsearch invalidates the cache for those shards automatically.
Enable the shard request cache per index during creation or update:
PUT /metrics-7d
{
"settings": {
"index.requests.cache.enable": true
}
}It's on by default in modern Elasticsearch versions, but verify with:
GET /metrics-7d/_settingsCheck cache performance similarly to the node query cache:
GET /_nodes/stats/indices/request_cache?humanHigh hit rates on shard request cache mean your queries are stable and your data windows are well-defined. Low rates suggest either query variation (different aggregations each time) or high write volume invalidating the cache frequently.
Field data cache for text fields
The field data cache is where many teams run into memory surprises. When you sort or aggregate on a text field without a keyword sub-field, Elasticsearch loads the entire inverted index for that field into memory as a data structure called a field data cache. This is expensive—a text field with high cardinality can consume gigabytes per node.
The classic mistake: aggregating directly on a text field:
{
"aggs": {
"products": {
"terms": {
"field": "product_name"
}
}
}
}If product_name is a text field without a keyword sub-field, Elasticsearch builds the field data cache on first access. On a 100GB index, this can consume several GB of heap per node. The cache stays resident and grows over time as unique values accumulate.
The solution is keyword fields. Define your mapping to include a keyword sub-field for aggregations:
PUT /products
{
"mappings": {
"properties": {
"product_name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}Then aggregate on the keyword version:
{
"aggs": {
"products": {
"terms": {
"field": "product_name.keyword"
}
}
}
}Keyword aggregations use a different execution strategy that doesn't require field data caching. They're faster and more memory-efficient.
Monitor field data cache with:
GET /_nodes/stats/indices/fielddata?humanLook for the memory_size_in_bytes metric. If it's growing unbounded, you likely have text field aggregations without keyword sub-fields. Field data cache eviction is LRU-based and controlled by indices.fielddata.cache.size (default 40% of heap), but evicting and rebuilding is expensive—prevention through proper mapping design is far better.
Common mistakes
Disabling caches to fix "out of memory" issues. When heap pressure mounts, teams often disable the node query cache or shard request cache thinking it'll free memory. This reduces immediate symptoms but usually makes things worse: queries run slower, cache pressure shifts elsewhere, or the cluster eventually OOMs anyway because queries are now inefficient. Measure first, understand what's filling memory, then act.
Aggregating on unmapped text fields. Adding a new aggregation on a field that's never been aggregated before can suddenly spike memory usage if that field is text without a keyword sub-field. Always check mappings before adding aggregations to production dashboards.
Assuming cache settings apply immediately. Cache configuration changes (like indices.queries.cache.size) take effect when the cluster restarts or settings are reapplied. Existing cached entries don't disappear—they evict according to their current policy. Plan cache tuning during maintenance windows.
Ignoring cache invalidation on write-heavy indices. The shard request cache is worthless on indices with frequent writes. If you're caching aggregations on an index that updates every second, you're allocating memory for near-zero benefit. Separate your query patterns: use the shard request cache for time-series backfill queries, not for real-time hot data.
Key takeaways
- Node query cache stores filter bitsets in memory on each node, reusing them for repeated filters. Monitor hit rates with
GET /_nodes/stats/indices/query_cacheto validate cache utility. - Shard request cache only works on static data; writes invalidate the cache immediately. It's powerful for dashboards on time-windowed indices, not on real-time hot data.
- Field data cache loads text inverted indices into memory for aggregations. Use keyword sub-fields instead of aggregating directly on text to avoid costly field data builds.
- Never disable caches to fix memory issues—measure what's consuming heap first. Field data cache size and query patterns are usually the culprit, not the caches themselves.
- Cache hit rates are diagnostic tools. High rates on query cache mean filters repeat; high rates on shard request cache mean your data is stable; high field data cache size means your mappings need keyword sub-fields.
- Budget heap conservatively. Query cache defaults to 10%, shard request cache has no global limit (evicts per index), and field data defaults to 40%. Monitor actual usage and adjust based on your workload, not defaults.






Comments
Loading comments…