The Point-in-Time API for Consistent Pagination in Elasticsearch
AI generated
_doc
_index
Elasticsearch · Pagination
The Point-in-Time API for Consistent Pagination
stable snapshots for search_after-based deep pagination

Classic page-by-page pagination with from and size works reliably for the first few result pages, but becomes expensive with deep pagination and returns inconsistent results the moment the underlying data set changes between two page fetches. For export functions that need to reliably and completely walk through thousands or millions of documents, that is not good enough. The point-in-time API solves this by capturing a consistent snapshot of the index at a fixed moment, against which search_after can then paginate efficiently and without duplicates or missing documents. How this combination works technically, how it differs from the older scroll mechanism, and how a robust export function can be built on top of it is what this article covers.

11 min read Point-in-Time · PIT search_after · Deep Pagination

1. The problem: deep pagination with from and size

Simple pagination through from and size requires Elasticsearch to fully sort and materialize every hit up to the requested position on every single request, even when only a small subset of it actually gets returned. On page one this adds hardly any overhead, on page two hundred with fifty hits per page, ten thousand documents already need to be sorted before the fifty actually wanted are delivered. Past a certain depth, Elasticsearch even refuses such requests outright by default, controlled through the index.max_result_window setting.

A second, subtler problem concerns consistency: if a new document is inserted between fetching page one and page two, and it sorts ahead of the current position, every subsequent hit shifts by one position. The result is documents delivered twice or skipped entirely, a problem that is practically unavoidable during a stable export run over an actively changing index.

2. Creating a point-in-time snapshot

A point in time is opened through a dedicated endpoint and returns a pit_id that gets used in subsequent search requests instead of the index name. Internally, Elasticsearch retains the relevant segments across the involved shards, so later search requests get evaluated against exactly the data state at the moment the snapshot was created, regardless of any write operations happening on the live index in the meantime.

The keep_alive parameter determines how long Elasticsearch retains the segments needed for the snapshot before automatically releasing them. Every search request that uses the point in time automatically extends this window by the specified value, so an active export run does not get cut off by a fixed time limit as long as it keeps issuing further requests regularly.


POST /products/_pit?keep_alive=2m

// response:
{
  "id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYQ..."
}

3. Combining search_after with point in time

For actual pagination, the point in time gets embedded in the pit object of the search request, while search_after passes the sort value of the last document from the previous page. This lets Elasticsearch skip the expensive full sort up to the requested position and jump efficiently straight to the next matching range, which delivers roughly constant response time even at very deep pagination.

For search_after to yield unambiguous results, the sort needs to include a unique tie-breaker field, usually _shard_doc or the document ID, since otherwise the order among documents with identical sort values would not be deterministic, and documents could appear twice or go missing.


GET /_search
{
  "size": 100,
  "query": { "term": { "category": "power_tools" } },
  "pit": { "id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMA...", "keep_alive": "2m" },
  "sort": [
    { "price": "asc" },
    { "_shard_doc": "asc" }
  ],
  "search_after": [39.90, 184023]
}

4. Difference from the classic scroll mechanism

The older scroll mechanism also produces a consistent snapshot, but it retains additional internal state per active scroll context on every involved shard, which adds up with the number of concurrently open scrolls and can create noticeable memory pressure. Scroll was originally designed for long-running but essentially sequential export operations, and it is explicitly not meant for interactive pagination with changing sort criteria.

Point in time combined with search_after is considerably more resource-friendly, since the snapshot state is lighter and can also be flexibly combined with different search requests against the same snapshot, for instance to apply different filters or sorts against the same consistent data state. Elastic now recommends point in time as the preferred replacement for scroll for the vast majority of use cases that require a stable snapshot.

5. Keep-alive behavior and resource usage in detail

Every open point in time pins segments on the involved shards that Elasticsearch would normally release and delete as part of merges. As long as the snapshot stays active, these segments remain, which, with very long-lived point-in-time contexts under simultaneous heavy write load, can lead to growing disk space usage, since deleted or overwritten documents in the old segment still need to physically persist.

A sensible keep_alive value should be based on the expected time between two consecutive requests plus a safety margin, not on the total duration of the entire export run. Since every request automatically extends the window, a value far shorter than the expected total runtime is usually sufficient, what matters is only that no more time passes between two requests than the configured value.

6. Practical example: a robust export function for large result sets

A typical export function opens a point in time at the start, then repeatedly runs search requests in a loop with an increasing search_after value, writing every result page directly to an output file or stream instead of collecting all results in application memory. As soon as a response contains fewer hits than the requested page size, the end of the result set has been reached and the loop can terminate.

After completing the export, the point in time should be explicitly closed through a DELETE call, rather than relying solely on the automatic expiry of the keep_alive window. This immediately frees the pinned segments instead of waiting for the timeout, and it matters especially for export functions that run frequently with many concurrent runs.


// After a successful export: close the point in time
DELETE /_pit
{
  "id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMA..."
}

7. Error handling: expired or invalid point-in-time IDs

If the keep_alive window expires, for instance because an export process paused unexpectedly long or crashed, every further search request with that point-in-time ID fails. A robust implementation should detect this failure case and then decide whether to restart the export completely with a fresh point in time, or whether resuming from the last successfully processed position makes sense, which however gives up the consistency guarantee of the original snapshot.

For distributed export processes that hold multiple point-in-time contexts open in parallel, it also pays off to monitor cluster statistics that report the number of currently open point-in-time contexts, in order to catch forgotten or improperly closed snapshots early, before they unnecessarily pin resources.

8. When from/size, scroll, or point in time each fit best

For shallow, interactive pagination with just a few pages, as needed by a typical result list in a web interface, simple from/size pagination remains the most pragmatic choice, since it requires no additional state and performs well enough for the first pages. Once deep pagination, full data exports, or batch processing across the entire index are required, however, point in time with search_after is the technically superior and Elastic-recommended solution.

The classic scroll mechanism remains functional but is now considered a legacy approach, worth using only in existing systems already built on it. For new implementations, there is hardly any way around point in time combined with search_after once consistent deep pagination is required.

9. Best practices for production use

A tightly sized but sufficient keep_alive value, explicitly closing every point in time after processing completes, and correct, unambiguous sorting with a tie-breaker field together form the foundation for reliable use. It also matters that application code is prepared for expired point-in-time IDs and does not blindly assume that a once-opened snapshot stays available indefinitely.

Regular monitoring of open point-in-time contexts through cluster statistics helps catch forgotten snapshots early, before they unnecessarily pin disk space on the involved shards. With these measures, point in time provides a robust, resource-friendly foundation for any application that needs to process large result sets consistently and completely.

Criterion from/size Scroll Point in Time + search_after
Consistency under changes Not guaranteed Guaranteed via snapshot Guaranteed via snapshot
Resource usage No additional state Per-shard state, often heavy Lighter snapshot state
Suitability for deep pagination Blocked past max_result_window Suitable but sequential by design Recommended standard approach
Flexibility of requests Free per request Only sequential paging forward Flexible filters against same snapshot
Status at Elastic Fine for shallow pagination Legacy, existing systems only Actively recommended for new systems

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

Point-in-Time API: The Essentials at a Glance

Core principle

A point in time captures a consistent snapshot of the index, against which search_after paginates efficiently without duplicate or missing hits.

Advantage over scroll

Lighter internal state per snapshot and flexible combination with different search requests against the same consistent data state.

Resource usage

Pinned segments remain in place as long as the point in time stays open, a tight keep_alive value and explicit closing matter.

Practical recommendation

For export functions and batch processing of large result sets, point in time with search_after is the Elastic-recommended standard approach.

11. FAQ: Point-in-Time API: The Essentials at a Glance

1What is the point-in-time API in Elasticsearch used for?
It creates a consistent snapshot of an index at a fixed moment, against which search_after can then paginate efficiently without duplicate or missing documents.
2How does point in time differ from classic scroll?
Point in time holds lighter internal state per snapshot than scroll and can be flexibly combined with different search requests against the same snapshot, while scroll is meant for purely sequential paging forward.
3What does the keep_alive parameter do for a point in time?
It determines how long Elasticsearch retains the segments needed for the snapshot. Every search request using that point in time automatically extends the window by the specified value.
4Why does search_after additionally need a tie-breaker field in the sort?
Without a unique tie-breaker field like _shard_doc or the document ID, the order among documents with identical sort values would not be deterministic, which can lead to duplicate or missing hits.
5Should a point in time be closed manually after the export?
Yes, an explicit DELETE call immediately frees the pinned segments instead of waiting for the keep_alive window to expire automatically.
6What happens if an export process pauses longer than the keep_alive value?
The point-in-time ID becomes invalid and every further search request with it fails. The application must then decide whether to restart with a fresh point in time.
7What resource cost does a long-open point in time cause?
The associated segments cannot be released through normal merges, which under high concurrent write load leads to growing disk space usage.
8Is from/size pagination completely outdated?
No, for shallow, interactive pagination with just a few pages it remains the most pragmatic choice. Point in time pays off once deep pagination or full exports are needed.
9How can you spot forgotten, unclosed point-in-time contexts?
Through cluster statistics that report the number of currently open point-in-time contexts, which should be checked regularly to catch unnecessarily pinned resources early.
10What keep_alive value should you choose for an export function?
A value based on the expected time between two consecutive requests, not on the total duration of the export, since every request automatically extends the window.