Reliable Cursor Pagination in FaunaDB FQL for Large Collections
Use FaunaDB FQL cursor-based pagination to read large result sets with stable latency. Start with paginate(Match(Index(...))), capture the cursor, and resume with the same cursor to avoid offset cost and scanning overhead.
15 Dec 2025, 17:11 UTC

You need to read a large, growing result set from FaunaDB without paying a growing cost per page or losing your place when new documents arrive. Cursor-based pagination with FQL's paginate gives you stable, constant-time page boundaries by encoding the last seen index position in a cursor token instead of using numeric offsets.
Desired outcome
Retrieve all documents matching an index predicate in repeatable pages, with consistent latency regardless of page depth, and resume safely if a page request fails.
Prerequisites
- A FaunaDB database with a key that has read access to the target collection and index.
- An index that covers the query predicate you need to page over. The index defines the sort order and the values used to build the cursor.
- A client that can run FQL, for example Fauna Shell or an official SDK. Run queries from an environment with network access to your FaunaDB endpoint and the key in scope.
Index selection directly affects pagination behavior. A suboptimal index can increase latency or change the cursor scope. Verify the index terms match the filter and sort you intend to use.
Focused procedure
1. Define the pagination entry point
Start with a Match over the index and wrap it with paginate. The first call establishes the initial cursor.
paginate(Match(Index('your_index')), { size: 100 })Run this in Fauna Shell or via your SDK with a key that can read the index. size controls the page size. Very large page sizes increase response time and network overhead; 100 to 1000 records is a common practical range.
2. Capture the cursor token
The response contains a data array and an optional after cursor. The cursor encodes the index, value, and collection reference needed to resume.
3. Request the next page
Pass the previous cursor back as the cursor argument. Numeric page numbers are not supported.
paginate(Match(Index('your_index')), { size: 100, cursor: })Replace with the token returned by the prior call. Repeat until the response no longer contains an after cursor.
4. Iterate with stable ordering
Because pagination is cursor-based, not offset-based, performance does not degrade with page number and the engine avoids scanning preceding rows. This makes it suitable for large-scale collections where offset scans become expensive.
Expected checks
- First page returns
dataand, if more results exist, anaftercursor. - Subsequent calls with the returned cursor return the next slice of documents in the same index order.
- Query latency remains roughly consistent across iterations when page size is constant.
- Document count per page matches the requested
sizeexcept for the final page.
A practical validation is to run the first paginate call, note the cursor, then execute a second paginate with that cursor and confirm the next page loads correctly. Measure latency and document count across multiple cursor iterations; consistent results confirm proper pagination behavior.
Limitations and recovery options
Cursor tokens become invalid if the underlying index definition is modified or if documents matching the index criteria are deleted in a way that changes the index ordering. In that case the next page request will fail and you must restart pagination from the beginning with a fresh first paginate call.
If a page request fails transiently, retry the same cursor. For long-running scans, consider checkpointing the last successful cursor so you can resume without re-reading earlier pages.
Do not change the index while a pagination session is active. If schema evolution is required, finish the current scan or accept a restart.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.