API pagination divides a result set into manageable responses. It can reduce payload size and help clients navigate large collections, but the design affects consistency, resource use, and access control. A next-page token is not automatically safe or stable simply because it looks opaque.
This guide explains the decisions behind reliable list endpoints. Start with the user’s navigation need and the underlying data behavior, then choose an approach the database and application can enforce. Do not assume one pagination method solves every changing dataset.
Define the API pagination contract
Specify the default and maximum page size, sort order, available filters, and response fields. Clients should know what happens at the end of the collection and how to request the next page. Keep limits enforceable on the server.
Choose whether total counts are required. Computing an exact total can be expensive or reveal information the caller should not receive. A navigation interface may work with a continuation indicator instead.
Document how changes during navigation affect results. Users should not infer a fixed snapshot unless the system actually provides that guarantee.
Choose offset or cursor behavior deliberately
Offset-style pagination can be straightforward, but large offsets and changing records can create performance or consistency concerns. Cursor-style pagination can support continuation from a known position, yet it still needs a clear ordering and token contract.
Choose based on the workload, supported navigation, and database access pattern. Jumping to an arbitrary page has different requirements from sequentially reading an event feed. Do not select a cursor merely because it sounds more modern.
The GitHub GraphQL pagination guide illustrates one cursor-based interface. Treat its details as that API’s contract, not a universal implementation for every backend.
Use a deterministic ordering
A sort field that contains ties can create ambiguous page boundaries. Include an appropriate tie-breaker so records have a deterministic relationship. For example, a timestamp alone may not distinguish several records created at the same instant.
Ensure the ordering matches the database query and cursor meaning. A token based on one order should not be accepted with an unrelated sort choice. Validate supported sorting fields through controlled application mappings.
Test tied values, empty pages, and boundary records. A happy-path collection with unique timestamps can hide the problems users encounter with real data.
Keep cursors bounded and validated
Treat an incoming cursor as untrusted input. Parse it through a maintained mechanism, enforce size and format limits, and reject unsupported values. Do not turn cursor contents into arbitrary query instructions.
If the token represents filters, version, or ordering state, bind or validate those fields according to the design. An opaque encoding is not automatically integrity protection. Use supported integrity mechanisms when tampering would create a security or consistency problem.
Decide whether cursors expire and how clients recover. A clear invalid-cursor response is better than silently restarting from an unrelated position.
Apply authorization throughout navigation
Every page must enforce the caller’s current permission to the collection and returned records. Possessing a continuation token should not grant access that the authenticated user lacks. Review multi-tenant filtering carefully.
Keep counts and continuation indicators within the same authorized scope. A response can leak information through metadata even when the visible rows are filtered correctly.
Our API object-level authorization guide explains the record boundary. Pagination is a performance and navigation feature, not an alternative to access checks.
Account for additions, updates, and deletions
Records can change between requests. Decide whether clients may see duplicates, miss newly inserted items, or require a supported snapshot mechanism. The appropriate behavior depends on the product’s use case and data store.
Avoid promising no gaps merely because a cursor is used. A record’s sort key can change, or a record can be removed. A cursor design must define these events rather than leaving the client to guess.
For exports or reconciliation tasks, consider the stronger consistency requirements separately from ordinary interactive navigation. A convenient list endpoint may not be a complete data-export contract.
Bound query work and response size
A maximum page size does not necessarily bound every database operation. Expensive filters, sorts, joins, or total counts can still consume substantial resources. Inspect representative query plans and indexes.
Return only the fields the client needs. Large embedded objects can make a modest number of rows an excessive payload. Review nested pagination when a response contains several independent collections.
Use appropriate rate, timeout, and concurrency controls for bulk readers. A valid sequence of small pages can still create significant load if many clients traverse the entire collection at once.
Test clients and maintain compatibility
Exercise first, middle, final, and empty pages; invalid tokens; maximum sizes; tied sort values; and permission changes. Confirm that clients stop correctly and do not repeat the same request indefinitely when continuation is absent.
Test concurrent data changes with synthetic records. Compare observed behavior with the documented contract and investigate unexpected duplicates or omissions. Keep the test repeatable enough to diagnose regressions.
Version meaningful token or ordering changes deliberately. A deployment that changes cursor interpretation without a compatibility plan can break long-running clients. Provide a safe restart path and communicate relevant limits.
A practical verification scenario
Consider a list of synthetic records where several share the same creation timestamp. Request small pages using the documented order and verify that the tie-breaker keeps boundary behavior deterministic. Then insert, modify, and remove test records between requests and compare the observed sequence with the API’s stated consistency contract.
Repeat the traversal with two accounts that have different permissions. Each account’s returned rows, count metadata, and continuation information should remain within its authorized scope. A cursor copied from another account should not become a route around that policy.
Test malformed and expired tokens without creating expensive repeated work. The client should receive a clear recovery path rather than loop on the same failed request. Record page limits, ordering, token interpretation, access context, and change behavior in the test evidence. This provides a much stronger implementation contract than a demonstration that the first page returned the expected number of items.
Frequently asked questions
Does an opaque cursor provide authorization?
No. The server must authenticate the caller and check access on every request. Encoding a token does not establish permission.
Is cursor pagination always a snapshot?
No. Consistency depends on the query and storage model. Document how records changing during traversal affect results.
What should I verify first?
Set page limits, define deterministic ordering, validate token inputs, and test boundaries under the actual authorization scope. Then measure query cost with representative data.