The Deep Pagination Problem in Elasticsearch

The Deep Pagination Problem in Elasticsearch

Why asking for page 5,000 of your search results is far more expensive than asking for page 1, what Elasticsearch does to protect itself, and how to move through large result sets safely using scroll, point in time, and search after.

Imagine a huge library with ten branches, each holding a different part of the collection. A visitor walks up to the main desk and says, “Please give me the books ranked 99,991 to 100,000 in your list of most popular titles.” To answer, the head librarian cannot simply look at one shelf. She must ask every branch for its top 100,000 books, gather all those lists on her desk, sort the giant pile, throw away 99,990 books, and hand over ten. The visitor gets ten books, but the librarian did a thousand times more work than for the first ten. This is exactly the deep pagination problem in Elasticsearch.

1What Is Deep Pagination?

Deep pagination means asking for a page of results that lies very far from the beginning of the sorted list.

When a search matches thousands or millions of documents, you almost never want all of them at once. You want a small window, such as ten or twenty results at a time. Moving from one window to the next is called pagination. Page one shows results one to ten, page two shows eleven to twenty, and so on.

Shallow pagination, such as pages one to five, is cheap and works beautifully in Elasticsearch. Deep pagination is when the requested page is far down the list, for example page ten thousand. The problem is that the cost of a page in Elasticsearch does not depend only on how many results you want. It also depends on how far down the list the page starts. The deeper you go, the more work the cluster must do to give you the same ten results.

Simple Analogy

Think of a very long queue at a railway ticket counter. Serving the first person is instant. But if you ask, “Show me person number 50,000,” someone must first walk the whole queue and count 49,999 people. The person you asked for is one person, yet the effort is almost the whole queue.

i
The Core Idea

In Elasticsearch, the price of a page grows with the position where the page starts, not just with the size of the page.

This guide explains why this happens, what safety rules Elasticsearch uses to stop you from hurting yourself, and which better tools exist when you truly need to walk through a big result set.

2The Basics: From and Size

Ordinary pagination in Elasticsearch uses two simple settings in a search request.

The size setting says how many results to return in this response. Its default is ten. The from setting says how many results to skip before starting to return. Its default is zero. So a request with from zero and size ten returns results one to ten. A request with from ten and size ten returns results eleven to twenty.

This is called offset pagination, because you give an offset, which is how many results to skip. Web developers love it because it maps directly to page numbers. Page number times page size, minus one page, gives the offset. It is also how most databases with an offset clause behave.

PagePage SizeFrom ValueResults Shown
11001 to 10
2101011 to 20
10109091 to 100
1,000109,9909,991 to 10,000
10,0001099,99099,991 to 100,000

The simplicity is the trap. From the outside, page one thousand looks identical to page one: same size, just a different number. But inside the cluster the two requests are very different in cost, as the next chapters will show.

Simple Analogy

Asking for “the tenth chapter of a book” is easy because chapters have bookmarks. Asking for “the ten-thousandth word” is different, because nobody has a bookmark there, and someone must count. Offset pagination is counting words.

3Search Is Distributed: Shards and the Coordinating Node

To see why deep pages hurt, we must first remember how a search travels through a cluster.

An Elasticsearch index is divided into shards. Each shard holds a slice of the documents and works like a small independent search engine. Because documents are spread over shards, any search must ask every relevant shard for its best results, because the true top ten could be hiding in any shard.

The request first lands on one node, which becomes the coordinating node for this request. Its job is to send the query to all shards, collect their answers, merge them into one final ordered list, and reply to the client. The coordinating node does not hold the data for this. It only organizes the merge.

flowchart TD
  CL["Client asks for page of results"] --> CO["Coordinating node"]
  CO --> S1["Shard 1: its own best results"]
  CO --> S2["Shard 2: its own best results"]
  CO --> S3["Shard 3: its own best results"]
  CO --> S4["Shard 4: its own best results"]
  CO --> S5["Shard 5: its own best results"]
  S1 --> M["Merge into one sorted list"]
  S2 --> M
  S3 --> M
  S4 --> M
  S5 --> M
  M --> R["Return the requested page"]
        
FIG 1 — Every shard is asked, and the coordinating node merges the answers
Simple Analogy

A newspaper wants the ten best photos from a contest that was judged by five separate panels. Each panel only saw its own entries. The editor must collect each panel’s finalists and choose the overall best ten. The editor is the coordinating node, and each panel is a shard.

This distributed design is what makes Elasticsearch scale. But it also means that no single shard knows what the global position of any document is. Only the merge step can decide that, and that is where deep pagination gets expensive.

4Query Then Fetch: The Two Phases of a Search

A search runs in two stages. Understanding them shows exactly where the extra work appears.

The normal search type is called query then fetch. In the first stage, the query phase, each shard runs the query and builds a small ranked list of the best matches. Each entry in this list is light: it holds only the document identifier and the values needed for sorting, such as the score. The heavy content of the document is not included.

The shard sends this light list to the coordinating node. The coordinating node merges the lists from all shards and works out the final order. Then comes the second stage, the fetch phase. The coordinating node asks only the shards that own the winning documents to send their full content, such as the source. Only the documents of the final page are fetched.

sequenceDiagram
  participant C as Client
  participant Co as Coordinating Node
  participant S as Each Shard
  C->>Co: Search with from and size
  Co->>S: Query phase: send me your top from plus size entries
  S->>S: Build ranked list of ids and sort values
  S->>Co: Return light list of entries
  Co->>Co: Merge all lists, skip from, keep size
  Co->>S: Fetch phase: send full documents for the winners
  S->>Co: Return the documents
  Co->>C: Return the page
        
FIG 2 — The query phase asks for from plus size entries per shard, the fetch phase gets only the final page

Notice a very important detail. In the query phase, each shard is asked for from plus size entries, not just size. That is the key to the whole problem, and we look at it closely next.

i
Good News

The fetch phase is cheap even for deep pages, because only the final ten or twenty documents are fetched. The expensive part is always the query phase.

5Why Deeper Pages Cost More: The Priority Queue

Every shard must keep a sorted waiting list that is as long as the offset plus the page size.

Suppose you ask for the page that starts at position ten thousand with size ten. A shard cannot know whether its own best result is close to the global top or far away. The only safe way is to provide its best ten thousand and ten results. Otherwise, if all the global results ranked 10,001 to 10,010 happened to live on one shard, that shard would fail to supply them.

So each shard builds a priority queue, which is a sorted list that always keeps the best entries seen so far, with a length of from plus size. The shard scans all matching documents, and for each one it decides whether it belongs among the current best entries. A longer queue means more comparisons and more memory.

Then all shards send their full lists to the coordinating node. The coordinating node builds its own priority queue with the combined entries and picks out the requested slice. It must hold entries from all shards at the same time.

flowchart LR
  subgraph SH["Each of the shards"]
    Q1["Queue of size from plus size"]
  end
  SH -->|"sends from plus size entries"| CQ["Coordinating queue: shards times from plus size entries"]
  CQ -->|"skip first from entries"| OUT["Keep only size entries"]
        
FIG 3 — Almost everything the cluster sorts is thrown away at the very end
Simple Analogy

Ten panels of judges must each send their best 100,000 photos, so the editor can be safe about which ones rank 99,991 to 100,000 overall. The editor receives a million photos, sorts them, and keeps ten. All the rest of the effort was needed for safety, but produced no output.

!
Key Insight

Deep pagination wastes work in proportion to the offset. The work is repeated in full for every new page, because the cluster does not remember anything from the previous request.

6A Worked Example With Real Numbers

Numbers make the problem impossible to ignore. Let us count how many entries the cluster must handle for different pages.

Take an index with five shards and a page size of ten. The table shows how many entries each shard must produce and how many entries the coordinating node must merge for a few different offsets.

Offset (from)Entries per ShardEntries Merged at Coordinator (5 shards)Entries Returned
0105010
10011055010
1,0001,0105,05010
5,0005,01025,05010
9,99010,00050,00010

Now see what happens when the number of shards grows. The offset is the same, at 9,990, and the page size is ten.

Number of ShardsEntries per ShardEntries Merged at Coordinator
510,00050,000
2010,000200,000
5010,000500,000
10010,0001,000,000

The cost has two multipliers. One is the depth, which is the offset. The other is the number of shards. A cluster with many shards suffers more from deep pagination, since every shard must send a full list.

Depth
Entries per shard grow with the offset
Shards
Merged entries grow with the number of shards
Repeat
The whole cost is paid again for every page
i
Simple Formula

Work at the coordinating node is roughly the number of shards times the offset plus the page size. Double either factor, and the work doubles.

7Where the Cost Lands: CPU, Memory, Network, and Pauses

The extra work is not abstract. It shows up as real pressure on several resources at once.

Resource

Processor Time

Each shard compares every matching document against a long queue, and the coordinating node sorts a very large merged list. Both use processor cycles that other searches could have used.

Resource

Heap Memory

Priority queues live on the Java heap of the data nodes and of the coordinating node. Large queues from many concurrent deep requests can fill the heap quickly.

Resource

Network

Every shard sends a long list of entries to the coordinating node. Even though each entry is small, hundreds of thousands of them add up to real traffic.

Resource

Garbage Collection

Big short-lived structures create heavy garbage. The Java process must pause to clean it, and long pauses make the node slow or even seem dead to the cluster.

The worst part is that the pressure spreads. A single expensive deep request on a coordinating node can slow all other requests on the same node. A few such requests at once can trip the memory protection circuits of the node, which then start refusing other requests too. Users who only wanted page one suddenly get errors, even though the guilty request was for page ten thousand.

Simple Analogy

Imagine one customer at a bank counter who asks the clerk to count all coins in the vault to find the ten-thousandth coin. The clerk is busy for an hour, and everyone else waiting in line for a simple deposit gets delayed as well.

!
Noisy Neighbor

Deep pagination is a classic noisy neighbor problem. One heavy user or one crawler can reduce the quality of service for every other user of the cluster.

8The Safety Fence: max_result_window

Elasticsearch does not let you walk off the cliff. It puts up a fence at ten thousand results.

Every index has a setting called index.max_result_window. Its default is 10,000. The rule is simple: the sum of from and size must not be larger than this value. A request for from 9,990 and size 10 is allowed, because the sum is exactly 10,000. A request for from 10,000 and size 10 is refused with an error that says the result window is too large.

The error message also tells you what to do. It points you to the scroll API for large data sets and to the search after feature for deep pagination, and it mentions that you could raise the setting. This message is the product’s way of teaching you the correct approach at the moment you hit the problem.

RequestFrom plus SizeResult
from 0, size 1010Allowed
from 9,990, size 1010,000Allowed, at the limit
from 10,000, size 1010,010Rejected with a result window error
from 0, size 20,00020,000Rejected, size alone is too large

Related windows

The same idea protects other features. There is a limit for how many top documents a rescoring step may work on, and a limit for the number of inner hits returned inside a parent result. Each limit exists to keep the amount of hidden work per request within reason.

Simple Analogy

The fence at the edge of a cliff does not stop you from enjoying the view. It stops you from a fall that hurts everybody who has to rescue you. Ten thousand is a generous safe distance for almost every human use.

i
Human Reality

Real people almost never look beyond the first few pages of results. A search that needs users to click through ten thousand results is usually a search that needs better filters or better ranking.

9Why Not Just Raise the Limit?

It is possible to increase the window. It is rarely a good idea.

The setting can be changed per index, and many developers do so the moment they see the error. It feels like a quick fix. But raising the limit only removes the fence. It does not remove the cliff. The cost still grows with the offset, and now there is nothing to stop a single request from asking for a million entries per shard.

Consider what happens if you set the window to one million. A single request with from 999,990 on an index of twenty shards asks the cluster to produce twenty million entries, hold them in memory at the coordinating node, and sort them. A handful of such requests, whether from a buggy client, a curious user, or an automated crawler, can consume the whole heap and take the node down.

When a Small Increase Can Be Acceptable

  • The index is small, with few shards and small documents
  • Only a trusted internal tool uses the deep pages
  • Traffic is very low and predictable
  • You have measured the memory and latency effect

Why It Is Usually Harmful

  • Every user or bot can trigger very expensive requests
  • Memory use grows with shards times the window
  • Latency of every page grows, not just the deep ones
  • It hides a design problem instead of solving it
ANTI-PATTERN-01 Avoid
Problem

The team sets the maximum result window to a very large number so that the application can show any page number without errors.

Why It’s Harmful

A single request for a deep page can use enormous memory. Automated crawlers that walk through page numbers can then bring the cluster down.

Correct Approach

Keep the default limit, and use a cursor-based approach for deep traversal. Limit the visible depth in the application.

10Sorting, Ties, and Bouncing Results

Pagination has a second, quieter problem. The pages themselves may not agree with each other.

Offset pagination assumes that the sorted list stays the same between requests. Page one and page two come from two separate searches. If anything changes between them, results can be shown twice or skipped. Three things commonly cause this.

New or Changed Documents

If a new highly ranked document is indexed between the request for page one and page two, everything shifts down by one. The last result of page one now appears again as the first result of page two. If a document is deleted, one result may be skipped entirely.

Ties in the Sort Value

When many documents share the same sort value, for example the same score or the same date, their relative order is not guaranteed to be the same in each request. A document could appear at the end of one page and again at the start of the next.

Different Copies of the Same Shard

Each shard usually has replica copies. Two copies can have slightly different internal segments because they were merged at different times, which can shift scores slightly. If request one is answered by the primary and request two by a replica, the results may differ. Users see this as results bouncing around from one page load to the next.

Simple Analogy

Imagine reading a long list on a notice board while someone keeps adding names at the top. When you look at the second half of the board, the names you had just read shift down and you read some of them a second time.

Reducing the bounce

For ties, always add a unique tiebreaker field to the sort, so that the order is fully decided. For different shard copies, use the preference option with a value tied to the user’s session, so the same user is routed to the same shard copies for every page. For changing data, use a frozen view of the index, which is what the point in time feature offers, and which we study soon.

!
Important

Fixing the cost of deep pagination does not automatically fix the consistency problem. A good pagination design solves both.

11Total Hits: The Hidden Second Cost

Page counters like “page 3 of 8,421” look harmless, but the number behind them can be costly to compute.

To show “page 3 of 8,421”, the application needs to know how many documents matched in total. Counting exactly requires visiting every matching document on every shard, and the search cannot stop early once it has found enough top results. On a big index, this counting can cost more than finding the top ten.

To avoid this, Elasticsearch by default tracks the total only up to ten thousand hits. If more than ten thousand documents match, the response says the total is at least ten thousand, not the exact number. This lets the engine skip work through smart shortcuts. You can ask for exact counts using the track_total_hits setting, or for a different threshold, but you pay for it in speed.

Setting ValueWhat You GetCost
Default (threshold of 10,000)Exact count up to 10,000, then “at least 10,000”Low
Custom numberExact count up to your numberGrows with the number
trueExact count alwaysHigh on large result sets
falseNo total at allLowest, allows early stopping
Simple Analogy

Answering “how many people live in this city?” exactly needs a full census. Answering “more than ten thousand” needs only a glance at a few streets. Most users are happy with the glance.

i
User Experience Tip

Show friendly messages such as “more than 10,000 results” instead of exact page counts. This is honest, cheap, and encourages users to refine their search instead of scrolling forever.

12The Scroll API: The Old Answer

Scroll was the first tool built for reading very large result sets.

The scroll API works in two steps. First you run a normal search and add a scroll parameter with a time, such as one minute. Elasticsearch then takes a snapshot of the index as it looks at that moment and keeps a search context alive on every shard. The response includes a scroll identifier. Then you repeatedly send this identifier to get the next batch. Each batch continues exactly where the last one ended, and the data does not change during the scroll, even if documents are added or deleted, because you are reading the frozen snapshot.

Scroll avoids the deep pagination cost, because each shard remembers where it stopped and simply continues. Nobody needs to rebuild a big queue for every batch.

sequenceDiagram
  participant C as Client
  participant Co as Coordinating Node
  participant S as Shards
  C->>Co: Search with scroll time and size
  Co->>S: Run query and keep search contexts open
  S->>Co: First batch and cursor positions
  Co->>C: Batch 1 with scroll id
  C->>Co: Send scroll id for next batch
  Co->>S: Continue from saved position
  S->>Co: Next batch
  Co->>C: Batch 2 with scroll id
  C->>Co: Clear scroll when finished
  Co->>S: Release contexts
        
FIG 4 — A scroll keeps state on the shards and moves forward from a saved position
Simple Analogy

Scroll is like putting a bookmark in a frozen copy of the book. Every time you come back, you continue from the bookmark, and the book never changes under your hands. The price is that the frozen copy must be kept safe on the shelf while you read.

i
Sort by Document Order

For bulk exports where ranking does not matter, sorting by the natural document order makes scroll fastest, because the engine does not need to score or compare documents.

13Why Scroll Is Not for Users

Scroll solved the cost problem but introduced a resource problem. It is meant for machines, not for web pages.

Each open scroll pins a search context on every shard, and this context keeps the segments that existed at the moment of the search from being deleted. Elasticsearch normally merges small segments and removes the old ones to save disk and memory. While a scroll is open, the old segments must stay, so disk space and file handles are held longer than usual. A busy index with many long scrolls can accumulate a surprising amount of stale data.

Limit

Open Context Limit

There is a limit for the number of open scroll contexts per node. If many users each start a scroll, the limit is reached and new requests fail.

Limit

Time Limit

Each scroll has a keep alive time that is renewed with every batch. Users who pause for a few minutes lose their place.

Limit

Forward Only

You can only move forward. There is no way to jump to a page or move backwards.

Limit

Stale View

The snapshot never shows new data, so a user paging through it sees a frozen picture of the past.

ANTI-PATTERN-02 Avoid
Problem

A web application opens one scroll per visitor to power the next page button of a search results page.

Why It’s Harmful

Every visitor holds open search contexts on all shards, and abandoned scrolls stay alive until they time out. The node reaches its context limit and holds old segments in memory and on disk.

Correct Approach

Use scroll only for backend jobs such as exports, copies, and reindexing, and use search after for user-facing pagination.

!
Housekeeping

Always clear a scroll when you finish reading it. Letting it expire on its own keeps resources locked for longer than necessary. Also use the most recent scroll identifier returned in each response.

14Point in Time: A Frozen View of the Index

Point in time separates the frozen view from the paging cursor. That separation is the secret of the modern approach.

A point in time, shortened to PIT, is a lightweight handle that freezes the state of an index, or a set of indices, at the moment you open it. Any search that uses this handle sees exactly the same documents, regardless of what is indexed, updated, or deleted afterwards. You open a PIT with a keep alive time, and you close it when finished.

Unlike scroll, a PIT does not carry a position or a result queue with it. It is only the frozen view. This makes it much lighter, and it means you can run any number of different searches against the same frozen view. Position tracking is left to another feature, the search after cursor, which we cover next.

flowchart TD
  O["Open PIT with keep alive"] --> ID["Receive PIT id: frozen view of the index"]
  ID --> S1["Search 1 using the PIT id"]
  ID --> S2["Search 2 using the PIT id"]
  ID --> S3["Search 3 using the PIT id"]
  S1 --> CL["Close PIT when done"]
  S2 --> CL
  S3 --> CL
        
FIG 5 — A PIT is a shared frozen view that many searches can use
Simple Analogy

A PIT is like taking a photograph of the shop shelves at closing time. You can then examine the photograph slowly, from any angle, and ask many different questions of it. New stock arriving the next morning does not change the picture in your hands.

i
Keep Alive Renewal

Each search that uses the PIT can extend its keep alive time. Also remember that the identifier of the PIT may change in a response, so use the latest one returned in each reply.

A PIT still holds old segments while it is open, so it should be closed when the job finishes. But because it does not carry a result queue on each shard, it is cheaper to keep open than a scroll.

15Search After: A Cursor Instead of an Offset

Search after replaces “skip this many” with “continue after this exact item”. That small change removes the depth cost.

Instead of telling Elasticsearch how many results to skip, you tell it the sort values of the last result you have seen. The request says, “give me the next ten results whose sort position comes after these values.” Because each shard can apply this as a filter on its own sorted data, it only needs to find the next ten results after that point, not the first ten thousand.

This is known as keyset pagination or cursor pagination. Every request uses from equal to zero, and the cursor carries the position. So the queue size at every shard is only the page size, no matter how deep you are in the list.

AspectOffset Pagination (from and size)Cursor Pagination (search after)
Entries per shardFrom plus size, grows with depthOnly size, constant
Cost of page 1 versus page 10,000Very differentAbout the same
Jump to any pageYes, within the windowNo, must move step by step
Limit of depthTen thousand by defaultNone
State kept on serversNoneNone, the client keeps the cursor
Needs a unique tiebreakerRecommendedRequired
Simple Analogy

Offset pagination is like saying “start reading from the 5,000th line of the book”, which forces someone to count from the beginning. Cursor pagination is like saying “start reading right after the sentence that ends with these words”, which lets the reader open the book to the right spot directly.

!
Price of the Cursor

You lose random access. Users cannot jump from page 1 to page 500 in one step. They can only go next or previous. This is fine for infinite scroll and load more buttons, but not for classic numbered page links.

16The Recommended Pair: PIT Plus Search After

Together, point in time and search after give cheap pages and a consistent view. This is the current recommended approach for deep pagination.

The PIT provides the frozen view, so documents do not shift while the user is paging. Search after provides the moving position, so each page costs the same. The two features complement each other, and neither alone solves both problems.

sequenceDiagram
  participant C as Client
  participant E as Elasticsearch
  C->>E: Open point in time with keep alive
  E->>C: PIT id
  C->>E: Search with PIT id, sort, size, no search after
  E->>C: Page 1 with sort values of each hit and PIT id
  C->>E: Search with PIT id, same sort, search after last hit values
  E->>C: Page 2 with sort values and PIT id
  C->>E: Repeat with newest PIT id and last hit values
  E->>C: Empty page means the end
  C->>E: Close point in time
        
FIG 6 — Each page uses the last sort values as the cursor, while the PIT keeps the data frozen

Step by step

1

Open the PIT

Choose a keep alive time long enough to cover the time between two page requests, not the whole job. Receive the PIT identifier.

2

Run the First Search

Send your query with the PIT identifier, a full sort definition, and the page size. Do not include an index name, since the PIT already knows it.

3

Store the Cursor

Every hit includes its sort values. Keep the sort values of the last hit, along with the latest PIT identifier from the response.

4

Ask for the Next Page

Repeat the same query and sort, and add the stored sort values as the search after position.

5

Detect the End

When a response contains no hits, you have reached the end of the result set.

6

Close the PIT

Release the frozen view as soon as you are finished so the cluster can clean up old segments.

i
Keep Everything Else Identical

Between pages, the query and the sort definition must stay the same. Only the search after values and the PIT identifier change.

17Tiebreakers: Making the Order Unique

A cursor only works if every document has a single, unambiguous place in the sort order.

Suppose you sort by date, and a hundred documents share exactly the same date. The last hit of page one has that date, and you ask for everything “after” that date. The engine cannot tell which of the hundred documents you already saw, so it may skip some or repeat some. To fix this, you add a second sort field that is unique for every document. This is the tiebreaker. With a tiebreaker, no two documents share the same full sort key, so the cursor points to exactly one position.

Option

Shard Document Tiebreaker

When you use a PIT, Elasticsearch automatically adds a hidden tiebreaker that identifies each document uniquely within the frozen view. This is the easiest and safest choice.

Option

A Unique Field of Your Own

Add an identifier that is unique across the index, such as an order number, stored as a keyword or a number with doc values enabled.

Avoid

The Built-in Id Field

Sorting on the internal id field is discouraged because it uses a lot of memory. Copy the id into a separate field instead if you need it.

Avoid

Non-unique Fields

Fields such as category, status, or score are shared by many documents and cannot break ties on their own.

Simple Analogy

If ten students have the same marks, a rank list needs a second rule, such as roll number, to place them in a fixed order. Otherwise a teacher who says “start from the student after Rahul with 90 marks” cannot say which of the ten Rahul is.

!
Sortable Fields Only

All fields in the sort must have doc values, so analyzed text fields cannot be part of the sort. Use keyword fields, numbers, or dates for the cursor.

18Choosing the Right Method

Four tools exist. The right one depends on who is reading and what they need.

flowchart TD
  A["Need results beyond the first few pages?"] -->|"No"| B["Use from and size"]
  A -->|"Yes"| C{"Who consumes the data?"}
  C -->|"A person in a user interface"| D{"Do they need random page jumps?"}
  D -->|"No: next and previous, load more"| E["PIT plus search after"]
  D -->|"Yes: numbered pages"| F["Cap the depth, refine filters, or use windows by date"]
  C -->|"A program: export, copy, reindex"| G["PIT plus search after, or scroll for simple jobs"]
        
FIG 7 — A simple decision path for choosing a pagination method
MethodBest ForDeep CostRandom JumpConsistent View
from and sizeFirst few pages of a user searchGrows with depthYesNo
ScrollBulk export and reindexing jobsLowNoYes, frozen snapshot
Search after aloneSimple deep paging where small inconsistencies are fineLowNoNo
PIT plus search afterDeep paging with a consistent view, user or programLowNoYes

For most modern applications the answer is short: use offset pagination for the first pages, and PIT with search after when you need to go deeper or when the view must be consistent. Scroll remains available and is fine for simple one-time jobs, but the documentation now recommends the newer pair for deep pagination.

i
Combining Methods

Some applications use offset pagination for the first five pages and switch to a cursor approach beyond that, hiding the difference from the user behind a load more button.

19Parallel Export: Slices

When you must read millions of documents fast, one cursor is too slow. Slicing lets several workers read at once.

A slice is a portion of the result set. If you ask for four slices, the result set is divided into four disjoint parts that together cover everything exactly once. Each worker reads one slice with its own cursor. Slicing is available for scroll, and it is also available for searches that use a PIT.

The split is decided by hashing a document identifier, so it is roughly even. Each worker sends the same query, the same PIT or scroll setup, and a slice number out of the total number of slices. The workers run in parallel and never overlap.

flowchart LR
  Q["One query, one frozen view"] --> W1["Worker 1: slice 0 of 4"]
  Q --> W2["Worker 2: slice 1 of 4"]
  Q --> W3["Worker 3: slice 2 of 4"]
  Q --> W4["Worker 4: slice 3 of 4"]
  W1 --> OUT["Combined complete export"]
  W2 --> OUT
  W3 --> OUT
  W4 --> OUT
        
FIG 8 — Slices split one large result set into independent parts that workers read in parallel
Simple Analogy

Instead of one person counting all the books in the library, four people each count one quarter of the shelves. Together they finish faster, and no book is counted twice.

i
Choose a Sensible Number

Use a number of slices that is not larger than the number of shards, and not more than the workers you can actually run. Too many slices only add overhead.

20Paginating Over Aggregation Buckets

Deep pagination does not only happen with documents. It also appears when you list very many groups.

A terms aggregation returns the top groups, for example the top customers by number of orders. It has a size but no offset. If you ask for a size of one million, each shard must build a huge table of counts, and the coordinating node must merge them, which is the same kind of problem in a different shape. The cluster also has a limit on the total number of buckets a single response may contain, to protect memory.

The tool for walking through many groups is the composite aggregation. It returns groups in a fixed order in small pages, and each response includes an after key, which is the cursor to give in the next request. This is keyset pagination for buckets. It also lets you group by several fields together.

NeedToolNotes
Top few groupsTerms aggregation with small sizeFast and simple
All groups, page by pageComposite aggregation with after keyStable order, constant cost per page
Very large numbers of groupsComposite, plus a filter on part of the key spaceSplit the work by ranges to run in parallel
Count of distinct values onlyCardinality aggregationApproximate but memory friendly
Simple Analogy

Asking for the ten biggest cities is easy. Asking for a list of all forty thousand towns is better done in pages of a hundred, with each page telling you which town to start from next. The composite aggregation is that paged list.

21Application-Level Strategies

The best fix for deep pagination is often in the application, not in the cluster.

Cap the Visible Depth

Show only a limited number of pages, such as the first fifty, and tell users to refine their search if they have not found what they need. Search engines used by the public do exactly this. Nobody browses to result 100,000.

Encourage Filters and Facets

Give users side filters such as brand, date, and price. Filtering reduces the result set so that the wanted item appears on an early page. Filters also make deep pages unnecessary.

Window by Time or Range

For logs and events, let users pick a time window. Paging inside a one-hour window is shallow, even if the total data is huge. To move on, shift the window.

Infinite Scroll and Load More

Change the interface from numbered pages to a load more button. This matches cursor pagination perfectly and feels natural on phones.

Cache Popular First Pages

The first pages of common searches are requested again and again. Caching them in the application or at the edge removes load from the cluster.

Separate Export From Browse

If users ask for all results as a file, run an export as an asynchronous background job with a cursor approach, and email or link the file when done. Do not build it from thousands of interactive page requests.

i
Ranking Beats Paging

If users regularly need to go deep to find what they want, the ranking is probably wrong. Improving relevance is a better investment than making deep pages cheaper.

22Designing Cursors in Your API

If you use search after, your own API must decide how to hand the cursor to clients.

The raw cursor is the list of sort values of the last hit, together with the PIT identifier. Exposing this directly to browsers is not wise, since it reveals internal details, and clients could tamper with it. A better approach is to wrap it in an opaque token: a single encoded string that your server creates and later decodes. The client just passes the token back, without understanding it.

1

First Request

The client asks for a page of results with a query and no token. The server opens a frozen view and returns the first page.

2

Build the Token

The server combines the view identifier, the last sort values, and a hash of the query into one encoded string, and signs it so it cannot be forged.

3

Next Request

The client sends back the token. The server checks the signature, decodes it, and runs the next search with the same query.

4

Expiry

If the frozen view has expired, the server can either open a new view and continue from the same sort values, accepting small inconsistencies, or ask the client to start over.

Going backwards

Search after moves forward. To go to the previous page, one common trick is to reverse every sort direction and use the first hit of the current page as the cursor, then reverse the returned list before showing it. Another simple approach is to keep the cursors of pages you have already visited in the client and reuse them when the user goes back.

!
Bind the Token to the Query

A token should only be valid together with the same query and sort. Including a hash of the query in the token prevents clients from mixing a cursor from one search with a different search.

23The Same Problem in Relational Databases

Deep pagination is not unique to Elasticsearch. Every database that supports offset paging has it.

In a relational database, an offset clause tells the engine to skip a number of rows. The engine cannot jump to that position. It must produce the rows in order and count past the skipped ones. On a big table, an offset of a million rows means reading and discarding a million rows before returning the ten you asked for. The well-known cure is keyset pagination, also called the seek method: filter by the last seen key using a condition such as “key greater than last key”, with an index on that key, and take the next ten.

ConceptRelational DatabaseElasticsearch
Skip-based pagingLimit with offsetfrom and size
Why it is slow deepReads and discards skipped rowsEvery shard builds and sends from plus size entries
Cursor-based pagingSeek by last key valuesearch after with sort values
Need for unique orderingAdd primary key to order byAdd a tiebreaker to the sort
Consistent viewTransaction snapshotPoint in time
Extra distributed costOnly with sharded databasesAlways, since data lives on many shards

The distributed nature of Elasticsearch adds a multiplier. In a single-node database, a deep offset wastes work in one place. In Elasticsearch, the waste is repeated on every shard and then again at the merge. That is why the ten-thousand limit exists by default.

Simple Analogy

Skipping pages in a paper phone directory by counting names is slow. Opening the directory at the letter where you last stopped, using the tabs on the edge, is fast. The tabs are the keys of keyset pagination.

24Where Pagination Lives in the Stack

Pagination decisions are made at several layers. Each layer can help or hurt.

flowchart TD
  U["Browser or mobile app"] -->|"page number or cursor token"| G["Gateway or backend API"]
  G -->|"validates depth, rate limits, builds query"| CO["Coordinating node"]
  CO --> D["Data nodes with shards"]
  G -.->|"caches popular first pages"| CA["Cache"]
        
FIG 9 — The backend API is the best place to protect the cluster from deep or abusive pagination
Layer

User Interface

Decides whether users see numbered pages, next and previous, or infinite scroll. The choice determines whether you can use cursors.

Layer

Backend API

Validates page size and depth, translates page requests into safe queries, manages cursors and PIT lifetimes, and applies rate limits.

Layer

Coordinating Nodes

Carry the merge cost. It helps to have enough memory on these nodes, and dedicated coordinating nodes can isolate the effect.

Layer

Data Nodes

Build the per-shard queues. Their search thread pools and memory are the second place where deep pagination hurts.

A good rule is that clients should never talk to Elasticsearch directly for user-facing search. A backend in between can enforce the maximum page size and depth in one place, and it can change the pagination method later without changing the client.

25Security: Deep Pagination as an Attack

A request that is expensive to serve but cheap to send is an invitation to abuse.

Deep pagination fits this pattern perfectly. A single short request with a large offset can consume a lot of memory and processing on the cluster. Attackers can send many such requests to slow or crash a service. But harm does not need bad intent. A search engine crawler that follows the next page links of a public site can walk to page after page, and slowly push the deep cost higher and higher.

Defense

Validate Input

Reject page numbers and sizes beyond a sensible limit at the API layer, before the request ever reaches Elasticsearch.

Defense

Rate Limits

Limit the number of search requests per user or per address in a time window, especially for paged and export requests.

Defense

Crawler Rules

Use robots rules and page link design so that crawlers are not invited to walk thousands of result pages.

Defense

Least Privilege

The account used by the application should only have the rights it needs, and the maximum window should stay at its default so that a leaked account cannot ask for extreme sizes.

!
Cursors Are Not Secrets

Cursor values contain real field values from your data. Do not expose raw sort values for sensitive fields to clients. Use signed opaque tokens and check that the user is still allowed to see the results each time.

Simple Analogy

A restaurant that allows any customer to order a thousand dishes with one sentence needs a rule at the door. The rule is cheap to enforce, and it protects the kitchen, so honest diners still get their meals.

26Monitoring, Logging, and Metrics

You can find deep pagination problems before users complain, if you watch the right signals.

SignalWhere to LookWhat It Suggests
Slow searches with large from valuesSearch slow log, which records the request bodySomeone is paging too deep
High heap use on coordinating nodesNode stats for JVM memoryLarge merge queues are being built
Long garbage collection pausesNode stats and logsLarge short-lived structures from deep requests
Circuit breaker tripsNode stats for breakersRequests are being refused to protect memory
Search thread pool queue or rejectionsThread pool statsExpensive searches are crowding out others
Result window errors in the application logsApplication logs and error ratesClients or bots ask for pages beyond the fence
Open scroll or PIT contextsNode stats for search open contextsCursors are not closed or jobs are stuck
Growing segments held after mergesSegment and disk metricsLong-lived scrolls or PITs pin old segments

Track at the application level too

Elasticsearch does not give you a ready-made chart of the offsets that users request. So log the requested page number and page size in your own backend. A simple chart of the ninety-fifth and ninety-ninth percentile of the requested depth quickly shows whether deep pagination is happening. Add an alert when a single client crosses a threshold of deep requests in a short time.

i
Slow Log Thresholds

Set search slow log thresholds for both the query phase and the fetch phase. Deep pagination shows up in the query phase, while huge stored documents show up in the fetch phase. Seeing which one is slow tells you the cause.

27Troubleshooting Guide

Common symptoms, their likely causes, and what to do first.

SymptomLikely CauseFirst Action
Error that the result window is too largeFrom plus size exceeds the limitSwitch to PIT and search after, or cap depth in the application
Page 1 is fast, later pages are slower each timeOffset cost growing with depthMove to cursor pagination for deep pages
The same item appears on two pagesData changed between requests, or tied sort valuesUse a PIT and a unique tiebreaker
An item is missing from all pagesItem shifted between pages after a changeUse a PIT so the view is frozen
Results change order on refreshDifferent shard copies answeringUse a stable preference value per user session
Error about too many scroll contextsScrolls opened but not clearedClear scrolls, shorten keep alive, move to PIT
Search after returns nothing or wrong itemsSort changed between pages or tiebreaker missingKeep the same sort and add a unique tiebreaker
Cannot sort on a field for the cursorField is analyzed text without doc valuesUse a keyword sub-field or a numeric field
Node heap spikes during exportsVery large page size or many parallel slicesReduce the size and slices, and pace the workers
Disk usage rises during a long exportOld segments pinned by an open scroll or PITSpeed up the job, and always close contexts
Simple Analogy

Like a doctor’s chart, a good troubleshooting table links each visible symptom to a small number of causes. Checking them in order is faster than guessing.

28Shard Count, Sizing, and Deep Pagination

Your index design decides how painful deep pages are. Shard count is the biggest lever.

Recall that the merge cost is proportional to the number of shards. An index with one hundred small shards will suffer from deep pagination ten times more than the same data in ten well-sized shards. Over-sharding is a common mistake, because each shard is a full search engine with its own overhead, and every search touches all of them.

Right-Size Your Shards

Aim for shards that are large enough to be efficient, often tens of gigabytes, instead of many tiny ones. Fewer shards reduce the merge cost of every paged request and of every search in general.

Target Only the Indices You Need

If your data is split into daily or monthly indices, limit each search to the date range the user is interested in. Searching all indices multiplies the number of shards involved.

Route Related Data Together

Custom routing can send all data of one customer to a single shard. Searches for that customer then need to visit only one shard, which removes the multiplier entirely for paged results within that customer.

Give Coordinating Work Room

Deep or wide merges use heap on the coordinating node. Keep enough heap, or use dedicated coordinating nodes so that data nodes are not disturbed.

i
Both Levers

Cursor pagination removes the depth multiplier, and good shard design reduces the shard multiplier. The most robust systems use both.

29Design Patterns That Work

These habits keep search fast and pagination safe.

Hybrid Pagination

Use offset pagination for the first few pages, where it is cheap and gives friendly page numbers. Switch to cursors beyond a chosen depth.

PIT Plus Search After With a Signed Token

Hide the cursor and view identifier behind an opaque, signed token in your API, with clear expiry rules.

Hard Cap on Depth and Size

Validate page size and depth at the gateway, and keep the cluster window at its default as a second line of defense.

Asynchronous Exports

Turn large downloads into background jobs that use slices and cursors, write to a file store, and notify the user when finished.

Session Affinity for Results

Pass a stable preference value tied to the user’s session so all pages are answered by the same shard copies.

Stable Unique Sort Key

Design your documents with a unique, sortable field, such as an event identifier with a time component, so tiebreaking is natural and cheap.

30Anti-Patterns to Avoid

These mistakes appear again and again in real incidents.

ANTI-PATTERN-03 Avoid
Problem

Using offset pagination with a very large from value to export a whole index.

Why It’s Harmful

Each batch is slower than the one before, the total cost grows with the square of the number of batches, and the window limit blocks the job anyway.

Correct Approach

Use PIT with search after, with slices if speed matters, or a scroll for a simple one-time copy.

ANTI-PATTERN-04 Avoid
Problem

Search after used with a sort that has ties and no unique tiebreaker.

Why It’s Harmful

Documents with equal sort values may be skipped or repeated between pages, and nobody notices until data is compared later.

Correct Approach

Add a unique field to the sort, or use the automatic tiebreaker that comes with a PIT.

ANTI-PATTERN-05 Avoid
Problem

Opening PITs or scrolls and never closing them, or using a very long keep alive time as a convenience.

Why It’s Harmful

Old segments cannot be removed, disk usage grows, and the node may reach its limit for open contexts.

Correct Approach

Use a short keep alive that is renewed with each request, and close the context when finished, including on error paths.

ANTI-PATTERN-06 Avoid
Problem

Changing the query or the sort between two pages of a cursor.

Why It’s Harmful

The cursor values belong to the old sort. Results become meaningless, with skips and repeats.

Correct Approach

Bind the cursor to the query and sort, and start a new cursor when the user changes filters or sorting.

ANTI-PATTERN-07 Avoid
Problem

Requesting the exact total hit count on every page of a big search.

Why It’s Harmful

Exact counting needs a full visit of all matching documents on all shards for every request, which slows every page.

Correct Approach

Keep the default threshold, show approximate counts, or count once and reuse the value for the session.

ANTI-PATTERN-08 Avoid
Problem

Letting clients supply page size and depth without any limit, and exposing page number links to crawlers.

Why It’s Harmful

One request or one crawler can consume a large share of cluster memory and slow every user.

Correct Approach

Validate and cap page size and depth in the backend, rate limit, and control crawler access to result pages.

31Best Practices and Common Mistakes

A checklist to keep beside you when designing paged search.

Best Practices

  • Keep the maximum result window at its default
  • Use from and size only for shallow pages
  • Use PIT plus search after for deep or consistent traversal
  • Always add a unique tiebreaker to sorts used with a cursor
  • Close every PIT and scroll, including when jobs fail
  • Validate and cap depth and size in a backend layer
  • Keep shard counts reasonable and search only needed indices
  • Log requested depth and alert on unusual values

Common Mistakes

  • Raising the window to hide the error
  • Using scroll for user-facing pages
  • Forgetting that sorted text fields cannot be used for cursors
  • Ignoring result consistency between pages
  • Counting exact totals on every request
  • Letting crawlers walk through result pages
  • Keeping very long keep alive times
i
Two Questions

Before building any paged interface, ask: who really needs page one thousand, and what would they do with it? The honest answers often remove the problem.

32Advantages and Trade-Offs of the Approaches

Every pagination method trades something for something else.

Offset pagination

Advantages

  • Simple to understand and to implement
  • Supports jumping to any page inside the window
  • Matches numbered page interfaces
  • No state to manage

Disadvantages / Trade-offs

  • Cost grows with depth and with shard count
  • Hard limit at the result window
  • Pages can shift when data changes
  • Vulnerable to abuse through large offsets

PIT plus search after

Advantages

  • Constant cost per page at any depth
  • Consistent view while paging
  • No depth limit
  • Works with slices for parallel export

Disadvantages / Trade-offs

  • No random page jumps
  • Needs a unique sort key and careful cursor handling
  • Open views hold old segments until closed or expired
  • More logic in the application

The summary is that cursor methods trade freedom of navigation for predictable cost. Since real users rarely jump around deeply, and machines never need to, that trade is nearly always a good one.

33Real-World Scenarios

These stories show the problem and its cure in familiar settings.

The Crawler Incident

An online marketplace shows numbered result pages with links to page after page. A search engine crawler starts following those links deeper and deeper on category listings. Response times slowly climb, and one afternoon the coordinating nodes run out of memory. The team caps the visible pages at fifty, adds rules for crawlers, and switches the load more button to a cursor. The incident does not return.

The Nightly Export

A reporting job exports all orders of the day using page after page of offset requests. The job gets slower and slower and finally fails at the ten thousand limit. The team rewrites it with a point in time and search after, with four slices running in parallel. The job now finishes in minutes and its memory use is flat.

The Duplicate Rows Mystery

Customer support notices that the audit log screen sometimes shows the same event twice and misses others. The cause is that many events share the same second in their timestamp, and new events arrive while agents click next. Adding a unique event identifier as a second sort field and using a point in time removes the confusion.

The Mobile Feed

A social application shows an endless feed sorted by time. Instead of page numbers, the app sends a cursor token with each request. Every request costs the same, whether the user has scrolled past ten posts or ten thousand, and nobody needs to see a total count.

The Log Search Console

Engineers search millions of log lines. The console asks for a time window first, then pages inside it with a cursor. To look further back, the engineer changes the window. Deep offsets never appear, and the cluster stays healthy.

“Do not ask the cluster to count to a million. Give it a bookmark and let it continue.”

34Myths About Deep Pagination

Some beliefs about pagination cause avoidable trouble. Let us test them.

MYTH-01 False
The Belief

Asking for ten results always costs the same, whatever page it is.

The Reality

With offset pagination, each shard must produce from plus size entries, so a deep page costs far more than the first page even though both return ten results.

What To Do

Measure latency by page depth and use cursors for deep pages.

MYTH-02 False
The Belief

The ten thousand limit is just a default that should always be raised in production.

The Reality

It is a safety feature that protects memory and latency. Raising it removes protection without removing the cost.

What To Do

Leave it alone and change the pagination method instead.

MYTH-03 False
The Belief

Scroll is the right tool for paging through results in a web page.

The Reality

Scroll holds resources on the cluster for each reader and is meant for batch jobs. The current guidance for deep pagination is search after with a point in time.

What To Do

Keep scroll for simple exports and use the newer pair for interactive use.

MYTH-04 False
The Belief

Search after removes the need for a consistent view, so a PIT is optional and pointless.

The Reality

Search after fixes the cost, not the consistency. Without a frozen view, documents added or removed between pages can still cause repeats and gaps.

What To Do

Add a PIT whenever the pages must agree with each other.

MYTH-05 False
The Belief

Only huge clusters need to worry about deep pagination.

The Reality

Even a small cluster can be brought down by a few deep requests, especially when the index has many shards. Small clusters have less headroom.

What To Do

Apply limits and cursor design from the start, whatever the cluster size.

35A Tale of Two Requests: Page 1 Versus Page 1,000

Let us put two requests side by side and follow every step. The only difference is the offset, yet the journey is very different.

Both requests ask for ten results, sorted by relevance, on an index with ten shards. Request A asks for page one, with an offset of zero. Request B asks for page one thousand, with an offset of 9,990. Both are allowed, since request B just fits inside the default window.

flowchart LR
  subgraph A["Request A: from 0, size 10"]
    A1["Each shard builds a queue of 10"] --> A2["Coordinator merges 100 entries"] --> A3["Fetch 10 documents"]
  end
  subgraph B["Request B: from 9,990, size 10"]
    B1["Each shard builds a queue of 10,000"] --> B2["Coordinator merges 100,000 entries"] --> B3["Fetch 10 documents"]
  end
        
FIG 10 — Same size and same output, but one thousand times more entries handled in the query phase
StepRequest A (Page 1)Request B (Page 1,000)
Queue length in each shard1010,000
Entries sent by each shard1010,000
Entries merged at the coordinator100100,000
Entries thrown away at the end9099,990
Documents fetched1010
Heap pressureTinySubstantial
Typical latencyMillisecondsMuch longer, and it grows with load

The fetch phase is identical, because only ten documents are needed. The query phase for request B does one thousand times more sorting work. And if fifty people request pages this deep at the same time, the shards and the coordinating node must build fifty such queues at the same time, in the same heap.

Simple Analogy

Two customers ask a shopkeeper for the ten best-selling items. The first asks for the top ten. The second asks for the ten items ranked between 9,991 and 10,000. In both cases the shopkeeper hands over ten items, but the second customer forced her to rank ten thousand products first.

i
Take-Away

Judge a search request by the work done inside the cluster, not by the size of the response. A tiny response can hide a very large amount of work.

36Thread Pools, Queues, and How Deep Requests Starve Others

Deep pagination hurts other users through shared queues. Understanding those queues explains the strange slowdowns.

Every data node has a fixed number of worker threads dedicated to search, and a waiting line called a queue for requests that arrive while all workers are busy. Each shard-level part of a search takes a thread for as long as it runs. A deep page makes each shard part run longer, because building a long queue takes time. That means threads are occupied for longer, and the waiting line grows.

When the waiting line is full, the node refuses new search work and returns a rejection to the caller. Ordinary, cheap searches are then rejected just because they arrived while a few deep requests were holding all the threads. The victims are innocent users of the search box, while the cause was somebody browsing page ten thousand.

1

Deep Requests Arrive

A few requests with large offsets reach the coordinating node and are fanned out to every shard.

2

Threads Stay Busy Longer

Each shard-level task takes much longer than usual, so worker threads are not freed quickly.

3

The Queue Fills

New cheap searches wait behind the long ones. Response times rise for everyone.

4

Rejections Begin

When the queue is full, new search work is refused, and clients see errors that say the cluster is too busy.

Built-in helpers

Elasticsearch has several features that soften the blow. The coordinating node merges shard results in batches instead of all at once, using a setting that controls the batch size, so that memory does not spike as much on wide searches. Searches can be given a timeout, and can return partial results if some shards are slow. Adaptive replica selection routes requests towards shard copies that respond faster. These help, but they do not remove the underlying cost of a deep page.

!
Do Not Retry Blindly

When clients receive rejections, they often retry immediately, which adds even more load. Use growing delays between retries and lower the request rate, or the overload will feed itself.

37Modeling Data for Painless Pagination

Good pagination starts in the mapping. A few small decisions make cursors reliable and cheap.

Cursor pagination depends on sorting, and sorting depends on doc values, the column-style structure that stores each field’s value per document. So every field that appears in your sort must be a type that has doc values, such as a keyword, a number, or a date. A field that is only analyzed text cannot be used.

Design

A Unique Sortable Identifier

Give every document a field that is unique across the index and sortable, so that it can serve as the final tiebreaker. A time-ordered identifier is ideal.

Design

A Timestamp for Natural Order

Most feeds, logs, and histories are read newest first. A date field with good precision is a natural primary sort key.

Design

Keyword Copies of Text

If users need alphabetical order by a title, keep a keyword version of the title with a normalizer for case, so sorting is predictable.

Design

Handle Missing Values

Decide whether documents without the sort value go first or last, and make it explicit, so the order does not surprise you.

Sorting by relevance score

Sorting by relevance is the default, and it works with cursors too. But scores can be almost equal for many documents, and they can differ slightly between shard copies. With a frozen view and a tiebreaker, relevance-sorted cursors work fine. Without them, expect some jitter. Also remember that users rarely need relevance beyond the first few pages, so a limit on depth for relevance-sorted searches is very reasonable.

i
Searching Many Indices

If a search covers several indices, the sort fields must exist in all of them with compatible types, or you must tell Elasticsearch how to treat missing mappings. Mismatched types are a common source of errors in cursor-based searches over time-based indices.

38Async Search and Other Ways to Reduce Pain

Sometimes a search is heavy for reasons other than paging. Asynchronous search can make such requests friendlier.

The async search API lets you submit a search and get back an identifier immediately instead of waiting for the final result. You can then poll for progress, and even read partial results while shards are still working. This is useful for large searches over many indices, or over slow storage tiers, where waiting on one long open connection is awkward.

Async search does not remove the cost of deep pagination. It changes only how you wait. But it pairs well with the other tools. For example, a heavy exploratory search can run asynchronously with a timeout, and the user sees progress and partial data while the cluster works in the background.

ToolWhat It Helps WithWhat It Does Not Fix
Async searchLong-running searches and slow responsesThe high cost of deep offsets
Search timeoutStopping a search that takes too longWasted work already done
Partial resultsReturning what is ready when some shards are slowConsistency of pages
Caching first pagesRepeated shallow requestsUnique deep requests
Search after with PITDeep, consistent traversalRandom page jumps
Simple Analogy

Async search is like leaving your name at a busy restaurant and getting a buzzer, instead of standing at the counter. The kitchen is just as busy, but you are not stuck in the line.

39Deep Pagination in Tools You Already Use

Many tools around Elasticsearch already handle this problem for you. Knowing how helps you use them correctly.

Kibana Discover

Discover does not let you page through millions of documents with numbered pages. It loads a limited sample of results within your chosen time range, and encourages you to narrow the time window or add filters. This is the time window strategy in action.

Reindexing

The reindex operation copies documents from one index to another. Internally it reads the source in batches using a scroll, and it can use slices to run in parallel. It does not use large offsets, which is why it can copy huge indices safely.

Client Library Helpers

Many official client libraries provide helper functions to scan or iterate through all results of a search. These helpers hide the scroll or cursor details and give you a simple loop. Prefer them over hand-written offset loops for exports.

Data Pipeline Inputs

Tools that read data out of Elasticsearch, such as pipeline input plugins and data movers, generally use scroll or cursor style reading with configurable batch sizes and slices. Check the documentation of your tool and tune the batch size so memory stays reasonable.

!
Check Your Tools

Home-made scripts and old integrations are the usual place where offset loops still live. Search your code for loops that keep increasing the offset, and replace them before they cause an incident.

40Testing and Benchmarking Your Pagination

Do not guess how your cluster behaves at depth. Measure it, and test correctness as well as speed.

1

Use Realistic Data and Shards

Test on an index with the same shard count, document size, and data volume as production. Deep pagination cost depends strongly on shard count, so a tiny test index will mislead you.

2

Measure by Depth

Time the same query at offsets such as zero, one hundred, one thousand, and near the limit. Record median and high percentile latency and the heap use of the coordinating node.

3

Add Concurrency

Run many requests at the same time, since the damage of deep pagination appears under concurrency, through queues and heap pressure.

4

Compare With Cursors

Repeat the same walk with a cursor. You should see roughly flat latency by depth. If not, check the sort and the tiebreaker.

5

Test Correctness

Walk through a whole result set with cursors while another process indexes and deletes documents. Count the documents you saw, check for repeats and gaps, and compare with the frozen view.

6

Test Failure Paths

Let a view expire in the middle of a walk, restart a node, and cancel a request. Verify that the application fails gracefully and closes what it opened.

i
Automate It

Keep this test as an automated check in your pipeline. It will catch regressions such as a changed sort, a removed tiebreaker, or a new endpoint that accepts unlimited page numbers.

41Migrating From Offset to Cursor Pagination

If your application already uses deep offsets, you can move to cursors safely in steps.

1

Find Who Goes Deep

Analyze logs to see how often, and by whom, pages beyond ten or twenty are requested. Most teams discover that almost nobody does, except bots and internal jobs.

2

Put a Cap in Place

Add validation in your backend for page size and depth, with a friendly message. This stops the worst abuse at once and buys time.

3

Fix the Sort

Add a unique tiebreaker to sorts and make sure all sort fields have doc values. Do this first, since the cursor depends on it.

4

Build the Cursor Path

Implement PIT plus search after behind a signed token, with expiry rules. Test it as described in the previous chapter.

5

Change the Interface

Replace numbered links beyond the shallow range with a load more button or next and previous links. Keep numbered pages for the first few pages if users like them.

6

Move Batch Jobs

Convert exports and syncs from offset loops to cursor loops with slices where needed.

7

Watch and Clean Up

Monitor depth, latency, heap, and open contexts. If you had raised the result window earlier, restore the default once no caller depends on it.

!
Do Not Break Old Clients

If public API clients rely on page numbers, keep the old parameters working for shallow pages, and return a clear error with guidance for deeper ones. Introduce cursors as a new option, and give clients time to move.

Simple Analogy

Changing a road system while cars are driving is done one lane at a time, with signs telling drivers about the new route. A pagination migration works the same way: old and new paths live side by side for a while.

42A Launch Checklist for Any Paged Search Feature

Use this list in design reviews and before release. Each item prevents a real incident that teams have met in production.

1

Is the Depth Limited?

Confirm that the backend refuses page sizes and depths beyond a documented limit, and that the cluster window is still at its default.

2

Is the Sort Deterministic?

Check that every sort ends with a unique field or the automatic tiebreaker, and that every sort field has doc values.

3

Is the View Stable?

Decide whether users need a frozen view. If yes, use a point in time with a sensible keep alive. If no, document the trade-off.

4

Are Contexts Closed?

Verify that every opened view or scroll is closed on success, on error, and on cancellation, and that a safety net closes abandoned ones.

5

Are Counts Cheap?

Make sure the total hit count uses the default threshold or is approximated, and is not recalculated on every page.

6

Are Abuse Paths Covered?

Confirm rate limits, crawler rules, and authentication on every search endpoint, including export endpoints.

7

Is It Observable?

Check that requested depth, latency, and error rates are logged and charted, and that alerts exist for heap pressure, rejections, and open contexts.

i
Make It a Habit

A checklist takes five minutes and prevents weekends of firefighting. Add it to your pull request template for any change that touches search.

43A Short Glossary of Pagination Terms

Keep these plain-language definitions handy. They make conversations with teammates and documentation much easier to follow.

Term

Offset

The number of results to skip before the page begins, set with the from parameter.

Term

Page Size

The number of results returned in one response, set with the size parameter.

Term

Result Window

The maximum value of offset plus page size that an index accepts, ten thousand by default.

Term

Query Phase

The first stage of a search, in which each shard returns light entries with identifiers and sort values.

Term

Fetch Phase

The second stage, in which full documents are retrieved only for the results that will be returned.

Term

Priority Queue

A sorted list that keeps only the best entries seen so far, used by shards and by the coordinating node.

Term

Cursor

A saved position, usually the sort values of the last seen result, from which the next page continues.

Term

Tiebreaker

An extra sort field that is unique for every document, so that the order is always unambiguous.

Term

Point in Time

A lightweight frozen view of an index that many searches can share to see the same data.

Term

Search Context

State kept on a shard for an open scroll or view, which pins the segments that existed at that moment.

Term

Slice

One disjoint portion of a result set, read by one worker so that several workers can read in parallel.

Term

Keyset Pagination

Another name for cursor pagination, borrowed from databases, where the next page is found by a key greater than the last seen key.

When a teammate says “we hit the window”, they mean offset plus size went over the limit. When they say “we need a cursor”, they mean the team should stop skipping and start continuing. Sharing this vocabulary makes design discussions shorter and clearer.

44Frequently Asked Questions

Q1What exactly is the deep pagination problem?

It is the fact that the cost of a page grows with how far down the results it starts. Each shard must build a list of from plus size entries, and the coordinating node must merge all of those lists, only to throw away everything except the final page.

Q2Why does Elasticsearch stop me at 10,000 results?

The default result window setting limits from plus size to ten thousand, to protect memory and latency. It is a safety limit, and the error tells you to use scroll or search after for deeper traversal.

Q3Is it safe to increase the maximum result window?

Only in narrow cases with few shards, low traffic, and trusted users. In general it just removes the safety fence, while the cost still grows with depth and shard count.

Q4What is the difference between scroll and search after?

Scroll keeps a snapshot and a position on the servers and is meant for batch jobs. Search after is stateless and uses the sort values of the last hit as the cursor. With a point in time, search after also gives a consistent view.

Q5What is a point in time?

A lightweight frozen view of one or more indices. Searches that use its identifier all see the same data, even if documents change afterwards. It must be closed when finished.

Q6Why do I need a tiebreaker for search after?

If several documents share the same sort values, the cursor cannot point to one exact position, so documents may be skipped or repeated. A unique second sort field, or the automatic one that comes with a point in time, fixes this.

Q7Can users jump to page 500 with search after?

Not directly. You can only continue from a known position. If you need jumps, cap the depth and offer filters, or use offset pagination for the shallow pages.

Q8How do I go back to the previous page with a cursor?

Either store the cursors of pages already visited and reuse them, or run the query with the sort order reversed starting from the first hit of the current page, then reverse the results before showing them.

Q9Why do results sometimes change between page loads?

Because the data may have changed, sort values may tie, or different copies of a shard may answer. Use a point in time, a unique tiebreaker, and a stable preference value per session.

Q10Does deep pagination slow down the fetch phase?

Not much. The fetch phase retrieves only the documents of the final page. The heavy cost is in the query phase, where every shard builds a long list of entries.

Q11Do more shards make it worse?

Yes. The coordinating node must merge from plus size entries from every shard, so the merge cost grows with the shard count. Right-sizing shards helps all searches.

Q12How can I export a whole index efficiently?

Use a point in time with search after, and use slices to read in parallel. A simple scroll is also fine for a one-time copy. Do not use large offsets.

Q13How can I page through aggregation buckets?

Use a composite aggregation. Each response includes an after key, which you send in the next request to continue from where you stopped.

Q14Should I show the total number of results?

Show an approximate value such as more than ten thousand. Exact counts require visiting every matching document, which is costly for large result sets and rarely valuable to users.

Q15How do I stop crawlers and abusive clients from paging deeply?

Validate and cap page depth and size in your backend, apply rate limits, and use crawler rules and interface design that do not invite bots to walk thousands of result pages.

Q16How can I detect deep pagination in production?

Use the search slow log, watch coordinating node heap and garbage collection, track breaker trips and rejections, and log the requested depth in your own application so you can chart and alert on it.

45Summary and Key Takeaways

The deep pagination problem exists because offset pagination in a distributed search engine forces every shard to build a sorted list as long as the offset plus the page size, and forces the coordinating node to merge all of those lists, only to keep a handful of results. The cost grows with the depth of the page and with the number of shards, and it is paid again for every page. Elasticsearch protects itself with a default result window of ten thousand. The healthy way forward is to use offset pagination only for shallow pages and to use a point in time with search after for deep traversal, with a unique tiebreaker, closed contexts, and limits at your backend. Like the librarian in our opening story, the cluster works best when you give it a bookmark to continue from, instead of asking it to count from the beginning every time.

Key Takeaways

  • Deep pages are expensive — each shard must return from plus size entries, so cost grows with the offset and with the number of shards.
  • The fence is a feature — the default result window of ten thousand protects your cluster, so change your method instead of raising the limit.
  • Use cursors for depth — search after keeps the cost per page constant by continuing from the last sort values.
  • Freeze the view with a PIT — a point in time gives consistent pages and an automatic tiebreaker, and must always be closed.
  • Scroll is for batch jobs — it holds resources for each reader, so keep it away from user-facing pages.
  • Protect from the outside — cap page depth and size in your backend, rate limit, and control crawlers.
  • Measure and design well — right-size shards, log requested depth, watch heap and slow logs, and remember that better ranking and filters remove most needs for deep pages.