skip to content

When should you still use Elasticsearch's scroll API instead of search_after with a point in time?

level: seniorimportance: should knowfreq 45%

answer

  1. It snapshots the index and walks forward only
  2. Built for exporting everything, not for page 3
  3. Each open cursor pins files on the data nodes
  4. A node-level ceiling limits how many can be open
  5. The newer answer pairs a frozen view with a cursor

basics

~20 s

Almost never for new code. Scroll is a stateful snapshot cursor built for exporting an entire result set once, and Elasticsearch no longer recommends it for deep pagination; search_after with a point in time covers the same ground statelessly. Scroll survives mainly in existing batch jobs.

solid answer

~50 s

Scroll opens a server-side context that snapshots the shards' segments and hands back a `scroll_id`; each subsequent `POST /_search/scroll` returns the next batch and refreshes the context. It was designed for **exporting or reprocessing every matching document once**, not for user-facing paging: the snapshot goes stale, you can only move forward, and each open context pins segment files on disk and consumes a search context slot (`search.max_open_scroll_context` defaults to 500 per node). Modern Elasticsearch recommends `search_after` with a point in time instead — it gives the same consistent snapshot without a per-consumer server-side cursor, and it can be sliced for parallel consumption. So the honest answer is: keep scroll where it already works in a batch job, sort by `_doc` if you do, always clear the context when finished, and reach for PIT plus `search_after` for anything new.

code

bash · 11 lines
bash
# start the scroll (legacy export path)
curl -X POST "localhost:9200/logs-2026/_search?scroll=1m" -H 'Content-Type: application/json' \
  -d '{ "size": 1000, "sort": ["_doc"], "query": { "match_all": {} } }'

# fetch each next batch with the newest _scroll_id
curl -X POST "localhost:9200/_search/scroll" -H 'Content-Type: application/json' \
  -d '{ "scroll": "1m", "scroll_id": "DXF1ZXJ5QW5kRmV0Y2g..." }'

# always release the context, including on the failure path
curl -X DELETE "localhost:9200/_search/scroll" -H 'Content-Type: application/json' \
  -d '{ "scroll_id": "DXF1ZXJ5QW5kRmV0Y2g..." }'

go deeper

for a junior

Know that scroll walks a frozen snapshot forward in batches and exists for exports, and that new code should use search_after with a point in time instead.

for a middle

Explain the mechanics: the scroll_id, the keep-alive, why _doc sorting is cheapest, and why a snapshot is right for exports and wrong for a live UI.

for a senior

Discuss the operational failure modes you have actually hit — pinned segments, leaked contexts, the node-level context ceiling — and how you would migrate an inherited scroll job to a sliced PIT walk.

for a principal

Own the bulk-access strategy: whether analytics consumers read from the search cluster at all, how export workloads are isolated from interactive search, and what the deprecation path for legacy scroll jobs looks like.

## What scroll is A scroll is a stateful cursor over a frozen view of an index. You issue a normal search with a `scroll` query parameter giving a keep-alive (`POST /logs/_search?scroll=1m`), and the response carries both the first batch of hits and a `_scroll_id`. Every follow-up request goes to `POST /_search/scroll` with that id and a fresh keep-alive; the response returns the next batch and, when the hits array comes back empty, the walk is over. The context is released with `DELETE /_search/scroll`. The defining property is the **snapshot**: the scroll sees the index as it was when the scroll started. Documents indexed, updated or deleted afterwards do not appear and do not disturb the sequence. That is exactly what a full export wants and exactly what a live search UI does not. ## Why it is no longer the recommendation for paging Elasticsearch's own guidance moved to `search_after` with a point in time for deep pagination. The reasoning is structural: - **Server-side state per consumer.** Every concurrent scroller occupies a context on every shard it touches. Ten thousand users "paging" with scroll would be ten thousand contexts; ten thousand users paging with `search_after` are stateless requests, apart from any shared PIT. - **Pinned segments.** A scroll context holds references to the segment files that existed at scroll start, so merges cannot reclaim them while it is open. A forgotten scroll with a generous keep-alive quietly holds disk space and file handles hostage until it expires. - **Node-level ceiling.** `search.max_open_scroll_context` (default 500) caps how many scroll contexts a node will hold; exceed it and new scrolls are rejected. This is how a batch job that never clears its scrolls takes down other batch jobs. - **Forward only, one consumer.** A `scroll_id` cannot be shared, rewound, or restarted from the middle. A PIT plus `search_after` gives the same consistency guarantee (the PIT freezes the view), keeps the paging position on the client, and can be **sliced** so several workers walk disjoint parts of one snapshot in parallel — the modern equivalent of sliced scroll. ## Where scroll still legitimately appears - **Existing batch pipelines.** Working export and migration jobs written against scroll are not defective; rewriting them is a choice, not an obligation. - **Clients and libraries that only implement scroll.** Some older tooling and language clients expose a scroll helper and nothing else. - **Internal reindexing machinery.** Bulk operations of that shape have long been driven by scroll-like snapshot iteration under the hood; as a user you invoke `_reindex`, `_update_by_query` or `_delete_by_query` rather than driving the cursor yourself, and those APIs already take a `slices` parameter for parallelism. ## Operating scroll safely If you are keeping a scroll-based job: - **Sort by `_doc`.** Scrolling does not need relevance order, and sorting by `_doc` lets each segment be walked in its natural order, which is the cheapest possible iteration. Adding a real sort makes the export markedly more expensive for no benefit. - **Size the batch honestly.** A batch of a few thousand documents amortizes round trips; a batch of a hundred thousand large documents blows up the fetch phase and the client's memory. - **Use a short keep-alive and refresh it.** The keep-alive only has to cover the gap between two requests, not the whole job. A one-hour keep-alive on a job that dies after two minutes leaves an hour of pinned segments. - **Always clear.** Wrap the walk so `DELETE /_search/scroll` runs on both success and failure paths. Most scroll-context exhaustion incidents are leaked contexts from crashed jobs, not legitimate concurrency. - **Send back the newest `_scroll_id`.** It can change between batches. ## The interview answer The question is usually a check on whether you know the recommendation has moved. Say plainly: scroll was for exports, never for interactive paging; deep paging today is `search_after` with a PIT; for bulk export, prefer a sliced PIT walk, or `_reindex`/`_update_by_query` with `slices` when the destination is Elasticsearch itself; and if you inherit scroll, the risks to manage are pinned segments, leaked contexts, and the node's context ceiling.

  • Why sort a scroll by _doc rather than by a real field?
    Scrolling an entire result set does not need ranked order, and `_doc` lets each segment be iterated in its natural storage order — the cheapest possible walk, with no sort collector and no doc_values reads. Any other sort adds per-document work on every batch for output nobody looks at in order.
  • What happens if a job crashes without clearing its scroll contexts?
    The contexts survive until their keep-alive expires, pinning the segments they reference so merges cannot reclaim the space, and occupying slots against `search.max_open_scroll_context` (default 500 per node). Repeat that across restarts and new scrolls start failing. Clearing on the failure path matters more than on the success path.
  • How do you export a large result set in parallel today?
    Open one point in time and give each worker the same query with a different `slice` id, then let each walk its own `search_after` sequence. All workers see the same consistent snapshot and their slices do not overlap. If the destination is another Elasticsearch index, `_reindex` with `slices` does the same thing server-side.

saying these in an interview costs you the question

  • Recommending scroll for user-facing pagination
  • Assuming scroll results include documents indexed after it started
  • Leaving scroll contexts to expire instead of clearing them
  • Thinking a scroll can be rewound or resumed by another worker
  • Believing scroll avoids the result window by being cheaper per page

context