Pagination (from/size, search_after, scroll)
Elasticsearch offers three ways to page through results — shallow from/size, cursor-based search_after, and the legacy scroll — each with very different cost and consistency trade-offs.
Why it matters
The naive “page N” pattern (from/size) silently degrades: fetching page 1000 forces every shard to build and sort a list of from+size hits, exhausting memory. Deep pagination, exports, and reindex jobs each need the right tool, or you hit the 10,000-result wall or OOM a coordinating node.
How it works
| Method | Best depth | State | Consistent snapshot? |
|---|---|---|---|
from/size | shallow (< 10k) | none | no |
search_after | unlimited, sequential | none (cursor in request) | with a PIT |
scroll | bulk export | server-side context | yes (frozen) |
from/size— each shard returnsfrom+sizesorted hits to the coordinator, which re-sorts and discardsfrom; bounded byindex.max_result_window(default 10,000).search_after— pass the last hit’s sort values as the cursor; requires a deterministic, unique tiebreaker sort (e.g._shard_docor_id). Pair with a Point In Time (PIT) for a stable view.scroll— opens a snapshot consuming heap/file handles forscroll=1m; superseded bysearch_after+PIT for most jobs, but still used by some clients.
Example
// page 1
{ "size": 20, "sort": [ {"date":"desc"}, {"_shard_doc":"asc"} ] }
// next page: feed the last hit's sort array
{ "size": 20, "search_after": [1717000000000, 42],
"sort": [ {"date":"desc"}, {"_shard_doc":"asc"} ] }
search_after re-queries from the cursor — O(size) per page regardless of depth, versus from:10000 which sorts 10,020 hits per shard.
Pitfalls
- Deep
from—from:50000multiplies memory across shards and tripsmax_result_window; never expose unbounded page numbers. search_afterwithout a unique tiebreaker — ties at the cursor boundary skip or duplicate rows; always append_shard_doc/_id.- No PIT — between pages, refreshes can shift results; open a PIT for export consistency.
- Leaked scrolls/PITs — un-cleared contexts pin segments and leak heap; always
DELETEthem (keep_aliveonly bounds the leak).