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
_sourcefields 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
_sourcefields (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
_sourcefiltering – 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.
- Hypothesis generation: Large
_sourcepayloads cause request‑breaker trips. - Collect metrics:
GET _cluster/stats?human=true GET _nodes/stats/indices,process,breakers?human=trueLook for
breaker.requestandbreaker.totalusage close to limits. - 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] - 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
_sourceloading phase consuming ~5 MB per doc. - Inspect network payloads using
tcpdump:sudo tcpdump -i any -s 0 -w scroll.pcap port 9200Analyze the captured request size with Wireshark – confirms >5 MB per scroll batch.
- Compare staging vs production: Staging uses a 8 GB heap and
_sourcefiltering, reproducing the query without errors. - 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
_sourcefiltering to fetch only needed fields. - Store large text in a separate
binaryorattachmentfield and retrieve it viastored_fieldswhen 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_exceptioninelasticsearch.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
_sourcesize 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_breakeralert 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
- Q: Why does increasing
index.max_result_windownot 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 andmax_context_size. - Q: Can I disable the request breaker entirely?
A: Settingindices.breaker.request.limittounlimitedis possible but highly discouraged. It removes a safety net that protects the node from OOM; instead, increase heap or reduce payload size. - Q: How does
search.max_context_sizediffer from the request breaker?
A: The request breaker caps a single request’s memory usage, whilesearch.max_context_sizecaps the total memory a search context (including scroll) may occupy across its lifetime. - Q: My production cluster has 64 GB heap and still sees
search_context_missing_exception. What else could be wrong?
A: Check for_sourcefield duplication (e.g., nested objects) and for aggressivekeep_alivevalues that keep contexts alive longer than necessary. Also verify thatindices.breaker.total.limithas not been lowered by other heavy workloads. - Q: Is there a way to stream large
_sourcefields without loading them into heap?
A: Use thestored_fieldsordocvalue_fieldsAPI 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.