GraphQL Pagination — Cursor-Based vs Offset-Based
Fast recall
Quick revision
Core answer
Whether you know offset pagination breaks under concurrent writes, whether you can implement Relay-spec `edges` + `pageInfo` + opaque cursors, and whether you can bridge it to Spring Data / JPA repositories.
Exposing database offsets (or row IDs) as the "cursor" — clients reverse-engineer them and break when you add a sort. Or storing offset pagination in a URL that clients bookmark and share; it rots within hours.
Explain why cursors should be opaque and encode the sort key + tie-breaker, how `ScrollSubrange` in Spring for GraphQL maps to JPA keyset pagination, and the subtleties of filter+sort stability with cursor-based pagination.
Interview-ready guide
Interview answer
GraphQL list pagination comes down to a choice between stability and simplicity. Offset pagination is simpler to implement but produces incorrect results under concurrent data changes. Cursor-based pagination is more reliable and performs better at depth.
Why offset pagination fails in practice:
- With offset pagination, if a row is inserted between page fetches, the window shifts — users see duplicates or skip items entirely; for infinite-scroll UIs with concurrent data changes, this is a poor user experience
- The instability is invisible in testing because it only manifests with real concurrent data changes; most test scenarios use static datasets
Cursor-based pagination — how it works:
- Cursors anchor to the last seen sort key plus a unique tie-breaker like the primary key, so the next page is always consistent regardless of what was inserted or deleted between fetches
- On the database side, it translates to a keyset query —
WHERE (created_at, id) > (?, ?)— which index-seeks directly to the right position;OFFSET 10000forces the database to scan and discard ten thousand rows, growing slower as pages increase
Relay Connection pattern:
- The Relay Connection shape —
edges,node,cursor,pageInfowithhasNextPage— is the standard that gives mobile and web clients one consistent pagination contract; it also provides room for edge-level metadata without breaking changes - Cursors must include a unique tie-breaker — sorting by
created_atalone produces non-deterministic results when rows share timestamps; compound(created_at, id)guarantees uniqueness
Spring Data caveat:
Pageablein Spring Data always generates OFFSET SQL under the hood — for cursor pagination, write explicit@Querymethods with keyset WHERE clauses
I recommend cursors for anything user-facing with concurrent data changes, and offset only for internal admin grids where page-jumping is a hard requirement and data consistency is lower priority.
GraphQL Pagination — Cursor-Based vs Offset-Based
Direct Answer
offset pagination uses skip/limit parameters and is simple to implement but unstable when data changes during pagination. cursor-based pagination uses an opaque pointer to the last-seen item and is stable, scalable, and recommended for infinite scroll or real-time data. The Relay Connection spec formalizes cursor pagination and is the de facto standard for GraphQL APIs.
Code Example
# Relay Connection spec for cursor-based pagination ---------------------------
type Query {
posts(first: Int, after: String, last: Int, before: String,
filter: PostFilter, sort: PostSort = CREATED_AT_DESC): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
nodes: [Post!]! # convenience for clients that don't need cursors
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String! # opaque base64 token -- clients must not parse
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Query: first page
query FirstPage { posts(first: 20) { pageInfo { endCursor hasNextPage }
edges { cursor node { id title } } } }
# Query: next page using endCursor from previous response
query NextPage { posts(first: 20, after: "eyJ0Ijox...") { ... } }
Common Mistakes & Pitfalls
Exposing database offsets (or row IDs) as the "cursor" — clients reverse-engineer them and break when you add a sort. Or storing offset pagination in a URL that clients bookmark and share; it rots within hours.
Check your understanding
Answer these without looking back at the article.
- How do you implement a keyset query for cursor pagination in Spring Data JPA?
- How do you encode a cursor in a way that is opaque to clients but decodable on the server?
- When would you combine both pagination strategies in the same schema?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.