How does pagination work in GraphQL with cursor-based connections?
Learn how GraphQL cursor-based pagination works with edges, node, cursor, and pageInfo, and why connections beat offset/limit on changing data.
Expected Interview Answer
Cursor-based pagination in GraphQL uses the Connections spec: instead of page numbers, each item comes with an opaque cursor, and clients request the first: N items after: <cursor> to fetch the next slice relative to a stable position.
A connection query returns edges (each with a node and a cursor) plus pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor). The cursor encodes a stable pointer — often a base64-encoded id or sort key — so results stay consistent even as items are inserted or deleted, avoiding the skipped and duplicated rows that offset/limit pagination suffers on changing data. Clients loop by passing the previous endCursor as the next after value until hasNextPage is false.
- Stable results when data is inserted or deleted mid-scroll
- Efficient for large datasets — no counting or large OFFSET scans
- Standardized shape (edges, node, pageInfo) via the Relay Connections spec
- Supports forward and backward paging with first/after and last/before
- Cursors are opaque, so the backend can change its keying without breaking clients
AI Mentor Explanation
Cursor pagination is like resuming a match commentary from the exact ball you last described rather than by over number. If a ball is re-counted or a wide is added, an over-number bookmark drifts, but 'continue after this precise delivery' always resumes cleanly. The cursor is that exact delivery marker, keeping the running commentary consistent no matter how the count shifts.
Step-by-Step Explanation
Step 1
Model the connection type
Define a Connection with edges (node + cursor) and pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor) per the Relay spec.
Step 2
Request the first page
Client calls the field with first: N; the server returns N edges plus pageInfo describing whether more data follows.
Step 3
Read the endCursor
Take pageInfo.endCursor from the response — an opaque token pointing to the last item returned.
Step 4
Fetch the next page
Call again with first: N, after: <endCursor>; the server resolves the cursor to a stable position and returns the following slice.
Step 5
Loop until done
Repeat, passing each new endCursor, until pageInfo.hasNextPage is false. Use last/before for backward paging.
What Interviewer Expects
- Knows the edges/node/cursor/pageInfo connection shape
- Explains first/after and last/before arguments
- Understands why cursors are stable vs offset/limit
- Knows cursors are opaque (often base64-encoded keys)
- Can describe the client loop using endCursor and hasNextPage
Common Mistakes
- Treating the cursor as a page number or array index
- Assuming cursors are human-readable or should be parsed by the client
- Forgetting pageInfo, so clients can't tell when to stop
- Believing offset pagination is equally stable on frequently-changing data
Best Answer (HR Friendly)
“Instead of asking for 'page 3', GraphQL cursor pagination asks for the next few items after a specific bookmark. Each item comes with that bookmark, so even if new data is added or removed while scrolling, you never accidentally skip or repeat items.”
Code Example
query Users($after: String) {
users(first: 10, after: $after) {
edges {
node {
id
name
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}async function fetchAllUsers(client) {
let after = null;
const all = [];
while (true) {
const { data } = await client.query({ query: USERS, variables: { after } });
const conn = data.users;
all.push(...conn.edges.map((e) => e.node));
if (!conn.pageInfo.hasNextPage) break;
after = conn.pageInfo.endCursor; // resume after last item
}
return all;
}Follow-up Questions
- How does cursor pagination avoid the skipped/duplicated rows that offset pagination suffers?
- What is typically encoded inside an opaque cursor?
- How do you implement backward pagination with last and before?
- How does the Relay Connections spec standardize this pattern?
- When might offset/limit pagination still be an acceptable choice?
MCQ Practice
1. In the Connections spec, which object tells a client whether more pages exist?
pageInfo carries hasNextPage, hasPreviousPage, startCursor, and endCursor, letting the client decide whether to keep paging.
2. What is a GraphQL cursor?
A cursor is an opaque token (often a base64-encoded id or sort key) that marks a stable position, so the backend can change its keying without breaking clients.
3. Why is cursor pagination preferred over offset/limit on changing data?
Because a cursor points to a stable position rather than a shifting offset, inserts and deletes don't cause rows to be skipped or duplicated across pages.
Flash Cards
What does a GraphQL connection return? — edges (each with a node and a cursor) plus pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).
How do you fetch the next page? — Pass the previous response's pageInfo.endCursor as the after argument alongside first: N.
What is a cursor? — An opaque token — often a base64-encoded id or sort key — marking a stable position in the result set.
Why cursors over offsets? — Cursors point to a stable position, so inserts/deletes don't skip or duplicate rows the way OFFSET does.