SOFTWARE / SYSTEMS / AIEngineering news. Technical depth.
Architecture / 4 MIN READ

Pagination when the dataset will not sit still

Choose cursor ordering and consistency guarantees that avoid surprising skips and duplicates while records are inserted, deleted, or updated.

For a changing dataset, use an opaque cursor over a deterministic keyset order and publish the consistency guarantee. Offset pagination is adequate for small, stable collections, but it cannot prevent shifts when rows are inserted or deleted before the next offset. A cursor is not magic: it must encode where the scan continues and, if promised, which snapshot it belongs to.

Start with the user-visible guarantee

There are three useful contracts. Best-effort traversal returns a moving view and may omit records whose order changes behind the cursor. Stable ordering promises that unchanged records appear in a deterministic sequence but still observes concurrent updates. Snapshot traversal fixes a read version so every page represents one logical moment, at the cost of snapshot storage, transaction lifetime, or revision-aware reads. Name the one your API provides.

Google’s AIP-158 pagination guidance specifies opaque page tokens, allows page_size to change on subsequent page requests, and expects all other arguments to remain consistent across pages; if they differ, the API should reject the request. Those rules keep clients from editing internal offsets and let servers evolve cursor representation. In practice, bind the token to filter, sort, tenant, page size policy, and snapshot or revision where applicable, and add integrity protection or encryption when you also need confidentiality.

Order by a unique, immutable tuple

A cursor such as created_at > last_seen skips or duplicates rows when timestamps collide. Use a tie-breaker: ORDER BY created_at DESC, id DESC, then encode both values. The next predicate must mirror lexicographic order. If the primary sort field can change, a record may move across the cursor during traversal. Either accept that under a moving-view contract, sort by immutable data, or hold a snapshot.

The GraphQL Cursor Connections Specification defines edges with cursors and page information, and requires consistent edge ordering across forward and backward pagination. Even outside GraphQL, its separation of nodes, cursors, and page metadata is useful. hasNextPage says more data was known under the server’s contract; it does not prove the dataset will remain unchanged.

Consider a feed ordered newest first. After page one, a new record arrives at the front. A keyset cursor continues after the last item seen, so the insertion does not shift later items. If an unseen record is deleted, traversal simply returns fewer results. If a seen record’s sort value changes and moves behind the cursor, it can appear again unless ordering is immutable or snapshot-bound. Write these scenarios into contract tests.

Keep totals and reverse traversal honest

An exact total count may refer to a different moment than the page and can be expensive. AIP-158 allows a total_size field and says that total may be an estimate if the API documents that behavior. Mark totals approximate, bind them to the snapshot, or omit them. For backward pagination, the GraphQL Cursor Connections Specification says edge ordering should stay the same under first/after and last/before; reverse the comparison logic needed to select the window, then restore presentation order rather than making clients reverse-engineer cursor contents.

Reject expired or mismatched cursors with a stable error and tell the client to restart traversal. AIP-158 permits expiring stored page tokens after a reasonable time and treats an empty next_page_token as the signal that the collection has ended. Avoid silently treating an invalid cursor as page one, which can duplicate processing. For batch jobs that must visit every record exactly once, prefer a snapshot export or durable work queue over an interactive pagination API.

Choose one list endpoint and document its ordering tuple, mutation behavior, token lifetime, filter binding, and invalid-token response. Then test inserts, deletes, ties, sort-field updates, and forward-to-backward transitions between every page.

Protect cursors as capabilities

A cursor can reveal internal keys, filters, or snapshot identifiers. Keep it opaque, avoid personal data inside, and remember that AIP-158 says page tokens indicate where pagination continues, not who is authorized to see the results. Enforce tenant and caller context on every page even when the token is valid. Test a cursor copied between tenants, reused after permission loss, altered byte by byte, replayed after expiry, and submitted with a different sort. Log rejection by reason without logging the cursor itself if it contains sensitive state.

Reviewed 2026-09-06.

SOURCES & REVIEW

Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.

Read our editorial approach ↗