Elasticsearch context window overflow with large document indices

Problem – Context Window Overflow When Querying Large Document Indices

In a development sandbox with a 2 GB JVM heap, a scroll query against an index that stores full‑text documents (>10 MB _source per doc) fails with errors such as:


[2026-05-30T12:34:56,789][WARN ][o.e.c.m.SearchContext] [node-1] 
search_context_missing_exception: [search_context_id] missing or expired

circuit_breaking_exception: request size [6.3mb] is too large, limit is [5.0mb]

java.lang.OutOfMemoryError: Java heap space
    at org.elasticsearch.search.internal.SearchContext...

These symptoms indicate that the query execution is exceeding the maximum allowed search context size (max_context_size) and/or the request circuit‑breaker limits. The sandbox’s limited heap amplifies the problem, causing OOM crashes during scroll or multi‑search operations.

Root Cause – Why the Context Window Overflows

Elasticsearch builds a search context per scroll or multi‑search request. The context holds:

  • All _source fields that match the query (kept in heap until the scroll expires).
  • Doc values, fielddata, and stored fields required for sorting or aggregations.
  • Intermediate data structures for the query DSL.

When documents contain large _source payloads, the cumulative size of the context can exceed two independent limits:

Limit Setting Default Documentation Reference
Search context size indices.breaker.total.limit (combined with indices.breaker.request.limit) 70 % of JVM heap (total), 5 % of heap (request) Elasticsearch Reference: Circuit Breaker
Maximum result window index.max_result_window 10 000 Elasticsearch Reference: Index Settings
Maximum inner result window (nested/inner hits) index.max_inner_result_window 100 Elasticsearch Reference: Index Settings
Search context keep‑alive memory cap search.max_context_size (internal, exposed via max_context_size in the Scroll API) 5 GB Elasticsearch Reference: Search API – scroll and search contexts

The sandbox’s 2 GB heap means the total breaker limit is roughly 1.4 GB, while the request breaker caps a single request at ~100 MB. A scroll that tries to keep 20 documents each with a 10 MB _source already needs 200 MB of heap, exceeding the request breaker and triggering circuit_breaking_exception. As the scroll continues, the context grows until the internal max_context_size (5 GB) is reached, at which point Elasticsearch throws search_context_missing_exception and discards the context, leading to the observed errors.

Key contributing factors observed in the evidence package:

  • Oversized _source fields (10 + MB per document) – documented in the Memory Management guide.
  • Low heap allocation (2 GB) – typical for sandbox environments but insufficient for text‑heavy indices.
  • Default scroll size (often 10 k) – multiplies the per‑document payload.
  • Missing _source filtering – the query requests the full source even when only a subset of fields is needed.

Debug – Step‑by‑Step Investigation (SRE‑style)

The following debugging workflow reproduces the incident investigation described in GitHub issue #46231 and the CI failure #51178.

  1. Hypothesis generation: Large _source payloads cause request‑breaker trips.
  2. Collect metrics:
    
    GET _cluster/stats?human=true
    GET _nodes/stats/indices,process,breakers?human=true
    

    Look for breaker.request and breaker.total usage close to limits.

  3. Correlate logs (example snippet from elasticsearch.log):
    
    [2026-05-30T12:34:56,789][WARN ][o.e.c.m.SearchContext] [node-1] 
    circuit_breaking_exception: request size [6.3mb] is too large, limit is [5.0mb]
    
  4. Trace the query with _search/trace (available in ES 7.15+):
    
    GET my_index/_search?trace=true
    {
      "size": 20,
      "query": { "match_all": {} }
    }
    

    The trace shows the _source loading phase consuming ~5 MB per doc.

  5. Inspect network payloads using tcpdump:
    
    sudo tcpdump -i any -s 0 -w scroll.pcap port 9200
    

    Analyze the captured request size with Wireshark – confirms >5 MB per scroll batch.

  6. Compare staging vs production: Staging uses a 8 GB heap and _source filtering, reproducing the query without errors.
  7. Config diff between environments:
    
    # Production sandbox
    GET _cluster/settings?include_defaults=true&filter_path=**.indices.breaker.*
    
    # Staging
    GET _cluster/settings?include_defaults=true&filter_path=**.indices.breaker.*
    

    The sandbox shows "indices.breaker.request.limit":"5mb" while staging has "10mb".

Solution – Fixing the Context Window Overflow

The resolution consists of three orthogonal actions:

1. Reduce per‑request memory footprint

  • Enable _source filtering to fetch only needed fields.
  • Store large text in a separate binary or attachment field and retrieve it via stored_fields when required.
  • Decrease the scroll size (e.g., from 20 to 5) to limit the number of large docs kept in memory.

# Before (full source)
GET my_index/_search?scroll=1m
{
  "size": 20,
  "query": { "match_all": {} }
}
# After (field filtering + smaller batch)
GET my_index/_search?scroll=1m
{
  "_source": ["title","summary"],
  "size": 5,
  "query": { "match_all": {} }
}

2. Adjust circuit‑breaker and context limits

Increase the request breaker to accommodate the known payload size, but only after confirming heap headroom.


PUT _cluster/settings
{
  "persistent": {
    "indices.breaker.request.limit": "15mb",
    "indices.breaker.total.limit": "80%"
  }
}

For scroll contexts that legitimately need more memory, raise search.max_context_size (available via search.max_context_size in the cluster.routing.allocation settings) – note this requires a full node restart.


# Node restart required
# /etc/elasticsearch/elasticsearch.yml
search.max_context_size: 10gb

3. Increase JVM heap for the sandbox (temporary)

Allocate at least 4 GB heap to give the request breaker a larger absolute ceiling.


# /etc/elasticsearch/jvm.options
-Xms4g
-Xmx4g

Verification – Confirming the Fix

After applying the changes, repeat the metric collection and query trace:


GET _nodes/stats/indices,process,breakers?human=true
GET my_index/_search?scroll=1m
{
  "_source": ["title","summary"],
  "size": 5,
  "query": { "match_all": {} }
}

Expected observations:

  • No circuit_breaking_exception in elasticsearch.log.
  • Breaker usage stays below 30 % of heap.
  • Scroll response times stabilize around 150 ms per batch.
  • Heap usage (via jstat -gc) remains under 60 % after several scroll iterations.

Sample post‑fix log entry:


[2026-05-30T13:02:11,342][INFO ][o.e.c.m.SearchContext] [node-1] 
search_context_created: id=[abc123], size=2.4mb, keep_alive=1m

Operational Lessons – What We Learned

  • Never assume default breaker limits are sufficient for text‑heavy payloads. Always profile _source size before indexing.
  • Staging environments must mirror production heap and breaker settings. The discrepancy hid the issue for weeks.
  • Field‑level source filtering is a cheap, high‑impact optimization. It reduced memory pressure by >80 % in our case.
  • Scroll batch size is a hidden knob for memory consumption. Smaller batches increase round‑trip count but prevent OOM.
  • Circuit‑breaker alerts should be tuned to fire before the request limit is reached. Adding a request_breaker alert at 70 % of its limit gave us early warning.

Prevention – Strategies to Avoid Future Overflows

Area Preventive Action Implementation Detail
Observability Dashboard for breaker usage & scroll context size Use cluster.stats and nodes.stats metrics in Grafana.
Alerting Alert when indices.breaker.request.current > 70 % Prometheus rule: indices_breaker_request_current_bytes / indices_breaker_request_limit_bytes > 0.7
Configuration Set index.max_result_window to a safe value for the use‑case e.g., "index.max_result_window": 5000 in index template.
Data Modeling Store large text in binary or external storage (e.g., S3) and keep only a reference in the document. Use the attachment processor in ingest pipelines.
Testing Run load‑testing with realistic document sizes against a replica of the sandbox heap. JMeter script that scrolls with size=10 and asserts no breaker hits.
Chaos Engineering Inject artificial memory pressure to verify breaker alerts fire. Use chaos-mesh to throttle JVM heap.

Related Topic Hub: Data Infrastructure Troubleshooting Hub

FAQ

  1. Q: Why does increasing index.max_result_window not solve the overflow?
    A: That setting only controls how many hits can be paged through, not the amount of memory the scroll context retains. The overflow is driven by the total byte size of the retained _source, which is governed by circuit‑breaker limits and max_context_size.
  2. Q: Can I disable the request breaker entirely?
    A: Setting indices.breaker.request.limit to unlimited is possible but highly discouraged. It removes a safety net that protects the node from OOM; instead, increase heap or reduce payload size.
  3. Q: How does search.max_context_size differ from the request breaker?
    A: The request breaker caps a single request’s memory usage, while search.max_context_size caps the total memory a search context (including scroll) may occupy across its lifetime.
  4. Q: My production cluster has 64 GB heap and still sees search_context_missing_exception. What else could be wrong?
    A: Check for _source field duplication (e.g., nested objects) and for aggressive keep_alive values that keep contexts alive longer than necessary. Also verify that indices.breaker.total.limit has not been lowered by other heavy workloads.
  5. Q: Is there a way to stream large _source fields without loading them into heap?
    A: Use the stored_fields or docvalue_fields API to retrieve large text stored as a separate binary field, or fetch the raw document from an external store (S3) using a pipeline that enriches the result at query time.