Request Cache and Shard Query Cache in Elasticsearch Explained
AI generated
_doc
_index
Elasticsearch / Caching Internals
Request Cache and Shard Query Cache Explained
two different caching layers for aggregations and queries

Elasticsearch ships with two structurally different, often confused caching mechanisms that both improve response time for repeated requests, but operate on completely different levels: the shard-level request cache, which stores complete search responses including aggregations on the node side, and the node query cache, often also called the shard query cache, which caches individual, recurring filter clauses at the shard level. Anyone trying to improve performance for a facet-heavy Magento store page with layered navigation needs to understand which of the two caches actually applies in which situation, how both get invalidated on write operations, and how query structure can be shaped deliberately so both caching layers get used optimally.

11 min read Request cache vs. query cache Invalidation on refresh Layered navigation optimization

1. Two different caches that are often confused

The term caching gets used in Elasticsearch for two structurally different mechanisms, which differ both in what they store and in when they actually apply. The shard-level request cache stores the complete, fully computed JSON response of a search request, including aggregation results, while the node query cache only holds the result of individual filter clauses as an efficiently compressed bitset at the segment level.

This distinction matters because both caches favor different request patterns: the request cache shows its strength mainly for identical, repeatedly executed aggregation requests, while the query cache can still apply even when two overall requests differ slightly, as long as a particular, reusable filter clause is part of both requests.

2. The shard-level request cache in detail

The request cache operates at the node level per shard and only stores a search request's result when size is set to 0, meaning no individual hit documents get returned, only aggregation results and the total hit count matter. This behavior is deliberate, since aggregations are often computed across a very large number of documents and are therefore comparatively expensive, yet frequently queried repeatedly in identical form.

The cache key consists of the exact request body combined with the shard state at the time of the request. Two syntactically different but semantically identical requests, for instance with the JSON fields in a different order, produce different cache entries in practice, which is why a consistent, always identically structured request layout is critical for a good hit rate.


GET products/_search
{
  "size": 0,
  "query": {
    "term": { "category_id": 42 }
  },
  "aggs": {
    "brands": { "terms": { "field": "brand.keyword", "size": 20 } },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 50 },
          { "from": 50, "to": 150 },
          { "from": 150 }
        ]
      }
    }
  }
}

3. The node query cache in detail

The node query cache, often also called the shard query cache, doesn't store the whole request but the result of individual, reusable filter clauses as a bitset per Lucene segment. Only clauses inside a filter context, not inside a scoring query context with relevance calculation, are eligible for this caching at all, because only there is the result independent of a specific score computation.

Elasticsearch doesn't automatically cache every filter clause, it uses an internal heuristic that favors clauses that have run against at least an eighth of an index's segments, suggesting a certain likelihood of reuse. Rarely executed or very specific filter clauses never make it into the cache in the first place as a result.


GET products/_search
{
  "query": {
    "bool": {
      "must": { "match": { "name": "hiking boots" } },
      "filter": [
        { "term": { "visible": true } },
        { "term": { "category_id": 42 } }
      ]
    }
  }
}

4. Which cache applies when: query structure decides

Whether the request cache applies depends entirely on whether the complete request, including all parameters, is identical to a previous request and size was set to 0. Even a single differing parameter, for instance a different value in the search term or a different sort direction, produces its own new cache entry, even if the vast majority of the request stays identical.

The query cache, in contrast, applies at the level of individual clauses and can therefore still provide benefits even when two overall requests differ significantly in other parts, as long as one particular filter clause, for instance excluding disabled products, appears identically in both requests. A deliberately consistent, reused formulation of common filters raises the hit rate of both caches equally.

5. Invalidation behavior on write operations

Both caches are tied to the Lucene segment state of a shard and get implicitly invalidated on every refresh that makes new segments visible, since the underlying segment state changes. In concrete terms: even without explicit purge commands, both caches lose at least part of their existing entries with every refresh cycle, because new or changed segments require a new caching basis.

With a default refresh interval of one second, this means that for heavily writing indices, both caches get rebuilt practically all the time and rarely reach a high hit rate. For predominantly read-heavy workloads with less frequent write operations, as is the case for most Magento product indices outside of reindex windows, the caches instead stay valid over noticeably longer periods.

6. Practical impact on facet-heavy store pages

Category pages with layered navigation typically run the same basic structure of aggregations for attributes, price ranges, and brands on every page load, combined with the same recurring base filters such as category ID and the visibility flag. This exact pattern, many identical or heavily overlapping requests using the same filter clauses, favors both caching layers at once particularly strongly.

If, instead, individual, customer-specific factors such as personalized price rules or customer group discounts get baked directly into the same aggregation request on every call, the request cache's hit rate drops substantially, since practically every request becomes technically unique as a result. A cleaner separation, where personalized aspects get applied only after the cached base request, preserves cache effectiveness far better.

7. Configuring cache size and measuring the hit rate

The size of the request cache can be configured via the node setting indices.requests.cache.size as a percentage of the heap, with a default of one percent that is often too tight for very aggregation-heavy workloads. The node query cache, on the other hand, is controlled via indices.queries.cache.size, set to ten percent of the heap by default.

The actual hit rate of both caches can be read out via the indices stats API per index, with separate counters for hits, misses, and the currently occupied cache size. A persistently low hit rate despite genuinely recurring requests usually points to an overly inconsistent query structure or too short a refresh interval, not necessarily to an undersized cache configuration.


# Query the hit rate of the request cache and the query cache per index
curl "localhost:9200/products/_stats/request_cache,query_cache?pretty" \
  | jq '.indices.products.total | {request_cache, query_cache}'

8. Practical optimization: query structure for better cache use

For good use of the request cache, aggregation requests should consistently be formulated with size equal to 0 whenever only aggregation results, not individual hit documents, are actually needed, and with an always identical, not dynamically reassembled JSON structure, for instance through a fixed field order in the application layer.

For the query cache, it pays off to consistently place frequently recurring, static filter conditions inside a separate filter block rather than in the scoring query part, even if both variants would functionally produce the same result. Only the variant inside the filter context gives the query cache any chance at all to reuse the affected clause independently of relevance scoring.

9. Pitfalls: real-time search versus cache effectiveness

A common pitfall is configuring a very short refresh interval for supposedly fresher search results, without considering that both caching layers constantly lose their basis as a result and effectively stop applying almost entirely. In most Magento contexts, a delay of a few seconds between a data change and its visibility in search is entirely uncritical and rarely justifies the associated loss of cache effectiveness.

A second, more subtle pitfall is accidentally mixing personalized and generic aggregations in the same request, which renders even well-intentioned optimization attempts ineffective. Anyone who consistently keeps generic, well-cacheable base data separate from individual, deliberately uncached additional information benefits far more reliably from both caching layers.

Aspect Shard-level request cache Node query cache Practical relevance
Stores Complete aggregation response Bitset of individual filter clauses Different granularity
Requirement size equal to 0 in the request Clause inside the filter context Both require deliberate query structure
Invalidation On every segment-changing refresh On every segment-changing refresh Both depend on the refresh interval
Configuration indices.requests.cache.size, one percent default indices.queries.cache.size, ten percent default Both adjustable via node settings
Ideal for Identical, repeated aggregation requests Recurring, static base filters Layered navigation benefits from both

Mironsoft

Search index setup, relevance tuning, and Magento search

Magento search that shows the wrong products first?

We set up Elasticsearch or OpenSearch for Magento cleanly, tune relevance and facets to the actual catalog, and optimize indexing processes for large catalogs.

Relevance Tuning

Match search results and facets to actual customer needs.

Search Migration

Guide a clean migration from Solr or MySQL search to Elasticsearch/OpenSearch.

Index Performance

Make indexing processes for large catalogs reliable and performant.

10. Summary

Request Cache and Query Cache: The Essentials at a Glance

Two layers

The request cache stores complete aggregation responses, the query cache stores individual, reusable filter clauses as a bitset.

Requirements

The request cache needs size equal to 0, the query cache needs clauses in the filter context rather than the query context.

Invalidation

Both caches depend on Lucene segment state and lose at least part of their validity on every segment-changing refresh.

Practical benefit

Facet-heavy category pages with consistent base filters benefit strongly, personalized aggregations should be kept separate.

11. FAQ: Request Cache and Query Cache: The Essentials at a Glance

1What's the fundamental difference between the request cache and the query cache?
The request cache stores complete aggregation responses per shard, the query cache stores the result of individual filter clauses as a bitset per segment.
2What requirement must a request meet for the request cache?
The size parameter must be set to 0, so only aggregation results get returned instead of individual hit documents.
3Which filter clauses are eligible for the query cache?
Only clauses inside a filter context, not inside a scoring query context with relevance calculation.
4When does a filter clause actually get added to the query cache?
When an internal heuristic detects the clause has run against a relevant share of an index's segments and therefore looks reusable.
5When do both caches get invalidated?
On every refresh that makes new or changed Lucene segments visible, regardless of explicit purge commands.
6Why does layered navigation benefit particularly from both caches?
Because category pages typically use the same aggregations and the same recurring base filters such as category ID and visibility.
7Why does the hit rate drop for personalized aggregation requests?
Because individual factors such as customer-specific price rules make the request technically unique, producing no reusable cache entry.
8How can cache size be configured?
Via indices.requests.cache.size for the request cache and indices.queries.cache.size for the query cache, each as a percentage of the heap.
9How can the actual hit rate be measured?
Via the indices stats API, which returns separate counters per index for hits, misses, and currently occupied cache size.
10Why can too short a refresh interval destroy cache effectiveness?
Because every refresh makes new segments visible and thereby changes the caching basis, so both caches barely apply with very frequent refreshes.