Cursor-Based Pagination
Cursor pagination represents a position in an ordered result set rather than an absolute offset.
A common GraphQL shape is:
users(first: 20, after: $cursor) {
edges {
node {
id
name
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
The cursor can represent a stable position based on an ordering field such as:
createdAt + unique ID
For example, suppose records are ordered by:
createdAt DESC, _id DESC
A cursor can encode the values of the last returned record. The next query can ask for records after that position according to the same ordering.
This is often more efficient and stable than large offsets, but cursor pagination is more complex to implement correctly.
A cursor should generally be treated as an opaque value by the client. The client should not need to understand whether it contains a timestamp, database ID, encoded tuple, or another internal representation.
A good cursor is:
- deterministic
- tied to a defined ordering
- opaque to clients
- safe to validate
- stable enough for the intended traversal model
Cursor pagination is not automatically better. The underlying database query must still have appropriate indexes.
- Cursors represent positions in an ordered dataset.
- Clients should generally treat cursors as opaque.
- Cursor pagination needs a deterministic ordering.
- Database indexes should support the cursor query.
What should a GraphQL client generally assume about the internal contents of a cursor?