Mastering GraphQL Pagination: From Basics to Advanced Cursor-Based Strategies
Hey there, GraphQL adventurers! If you’ve mastered the core concepts from my previous post (check it out here if you haven’t…
Mastering GraphQL Pagination: From Basics to Advanced Cursor-Based Strategies

Hey there, GraphQL adventurers! If you’ve mastered the core concepts from my previous post (check it out here if you haven’t: https://medium.com/@mariumgoraya13/mastering-graphql-a-comprehensive-guide-to-its-core-concepts-915cd25b0e51),,) you’re ready to level up. Today, we’re diving into one of the most crucial advanced topics: pagination.
Pagination is all about handling lists of data efficiently — think scrolling through your social feed or browsing search results. In GraphQL, it’s not just about fetching data; it’s about doing it smartly to avoid performance pitfalls. We’ll explore simple approaches, then build up to the powerful cursor-based model, with plenty of engaging examples along the way. No servers or languages here — just pure concepts to make you a pagination pro. Let’s paginate our way through this!
Why Pagination Matters in GraphQL
Imagine querying for a user’s friends in a massive social network. Without pagination, you might fetch thousands of records at once — crushing your app’s performance and user experience. Pagination lets clients request data in bite-sized chunks, like “give me the first 10 friends, then the next 10.”

GraphQL offers flexibility here, but not all methods are equal. We’ll cover:
- Offset-based: Simple but flawed for large datasets.
- ID-based: Better, using object IDs for navigation.
- Cursor-based: The gold standard-efficient, flexible, and future-proof.
We’ll use a Star Wars-themed schema (inspired by the official GraphQL docs) for examples, where characters have friends lists.
Starting Simple: Plurals and Basic Lists
The easiest way to expose lists in GraphQL is with plural types — fields that return arrays of objects.
type Character { id: ID! name: String! friends: [Character] }
Query for R2-D2’s friends:
{ hero(id: "2001") { # R2-D2’s ID name friends { name } } }
Response:
{ "data": { "hero": { "name": "R2-D2", "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" }, { "name": "Leia Organa" } ] } } }
This is straightforward — like grabbing the whole menu at once. But what if there are 1,000 friends? Your app slows down, and bandwidth wastes away. Enter slicing!
Slicing: Limiting the List Size
To avoid fetching everything, add arguments like first: Int to slice the list.
Updated schema:
type Character { id: ID! name: String! friends(first: Int): [Character] }
Query for the first two friends:
{ hero(id: "2001") { name friends(first: 2) { name } } }
Response:
{ "data": { "hero": { "name": "R2-D2", "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" } ] } } }
Analogy: It’s like ordering appetizers first instead of the full buffet. But to “paginate” further (get the next two), we need a way to continue from where we left off. That’s where pagination strategies shine.
Exploring Pagination Styles: Offset, ID, and Cursor
Now, let’s compare approaches to fetching the “next” page.
Offset-Based Pagination: The Classic (But Risky) Approach
Add an offset: Int argument to skip initial items.
friends(first: Int, offset: Int): [Character]
First page (first 2, offset 0):
{ hero(id: "2001") { friends(first: 2, offset: 0) { name } } }
Response: Luke and Han.
Second page (first 2, offset 2):
{ hero(id: "2001") { friends(first: 2, offset: 2) { name } } }
Response: Leia (and maybe more if the list grows).
Pros:
- Simple math, offset = page * limit.
Cons:
- Performance killer for large lists (e.g., databases scan from the start each time).
- Unstable: If a new friend is added mid-pagination, offsets shift, causing duplicates or skips.
- Security risks: Exposes total count indirectly, potentially leading to DOS attacks.
Real-world pitfall: In a live app, if Han unfriends R2-D2 between requests, your offsets break. Not ideal for dynamic data!
ID-Based Pagination: Using Object IDs for Navigation
Improve by using the last item’s ID as a starting point.
friends(first: Int, after: ID): [Character]
Assume friends are sorted by ID.
First page:
{ hero(id: "2001") { friends(first: 2) { id name } } }
Response:
{ "friends": [ { "id": "1000", "name": "Luke Skywalker" }, { "id": "1001", "name": "Han Solo" } ] }
Second page (after Han’s ID):
{ hero(id: "2001") { friends(first: 2, after: "1001") { id name } } }
Response: Next friends starting after ID 1001.
Pros: Avoids full scans; stable if IDs are sequential.
Cons: Assumes sortable IDs; deletions can create gaps; not opaque, clients might guess patterns
Cursor-Based Pagination: The Flexible Powerhouse
Cursors are opaque strings (often base64-encoded) representing a position in the list. They’re not IDs or offsets — they’re black-box tokens from the server.
Schema introduces edges and connections for richness.
Basic idea: Wrap list items in “edges” with cursors.
type FriendsConnection { edges: [FriendsEdge] }
type FriendsEdge { cursor: String! node: Character }
type Character { id: ID! name: String! friendsConnection(first: Int, after: String): FriendsConnection }
First page (first: 2)
{ hero(id: "2001") { name friendsConnection(first: 2) { edges { cursor node { name } } } } }
Response:
{ "data": { "hero": { "name": "R2-D2", "friendsConnection": { "edges": [ { "cursor": "Y3Vyc29yMQ==", "node": { "name": "Luke Skywalker" } }, { "cursor": "Y3Vyc29yMg==", "node": { "name": "Han Solo" } } ] } } } }
Second page (after the second cursor):
{ hero(id: "2001") { friendsConnection(first: 2, after: "Y3Vyc29yMg==") { edges { cursor node { name } } } } }
Response: Leia and beyond (if more exist).
Pros:
- Opaque cursors hide implementation (e.g., could be offset, ID, or timestamp).
- Stable: Handles inserts/deletes gracefully.
- Extensible: Add metadata like total count or page info.
Cons:
- Requires more schema complexity (edges, connections).
Enhancing Connections: End-of-List and Metadata
Cursors alone aren’t enough. How do you know you’ve hit the end? Add a connection object with metadata.
type FriendsConnection { edges: [FriendsEdge] totalCount: Int pageInfo: PageInfo }
type FriendsEdge { cursor: String! node: Character }
type PageInfo { hasNextPage: Boolean! endCursor: String }
Query with page info:
{ hero(id: "2001") { friendsConnection(first: 2) { totalCount edges { cursor node { name } } pageInfo { hasNextPage endCursor } } } }
Response:
{ "data": { "hero": { "friendsConnection": { "totalCount": 3, "edges": [ { "cursor": "Y3Vyc29yMQ==", "node": { "name": "Luke Skywalker" } }, { "cursor": "Y3Vyc29yMg==", "node": { "name": "Han Solo" } } ], "pageInfo": { "hasNextPage": true, "endCursor": "Y3Vyc29yMg==" } } } } }
Second page:
{ hero(id: "2001") { friendsConnection(first: 2, after: "Y3Vyc29yMg==") { edges { node { name } } pageInfo { hasNextPage } } } }
Response (if Leia is last):
{ "data": { "hero": { "friendsConnection": { "edges": [ { "node": { "name": "Leia Organa" } } ], "pageInfo": { "hasNextPage": false } } } } }
totalCount: Total items (optional).hasNextPage: Signals the end.endCursor: Next page’s starting point.
The Relay Connection Specification
The Relay project formalises this cursor-based pattern. Key rules:
- Use
edgeswithnodeandcursor. - Include
PageInfowithhasNextPage,endCursor, and optionallystartCursor. - Support
first: Intandafter: Stringarguments.
interface Character { friendsConnection(first: Int, after: String): FriendsConnection! }
type FriendsConnection { totalCount: Int edges: [FriendsEdge] pageInfo: PageInfo! }
type FriendsEdge { cursor: String! node: Character }
type PageInfo { hasNextPage: Boolean! endCursor: String }
Wrapping Up: Your Pagination Playbook
Pagination in GraphQL is a journey from simple slices to sophisticated cursor-based connections. Offset is easy but risky, ID-based adds stability, and cursor-based reigns supreme with flexibility and metadata. This playbook equips you to handle any list — whether it’s friends, posts, or galactic allies!
What’s your favourite pagination trick? Drop a comment below — I’d love to hear! Clap 👏, follow for more, and share on LinkedIn to spread the pagination wisdom. 🚀
메타데이터
- post_id
- 8cd8f59d92ee
- slug
- mastering-graphql-pagination-from-basics-to-advanced-cursor-based-strategies-8cd8f59d92ee
- url
- https://medium.com/@mariumgoraya13/mastering-graphql-pagination-from-basics-to-advanced-cursor-based-strategies-8cd8f59d92ee
- canonical_url
- https://medium.com/@mariumgoraya13/mastering-graphql-pagination-from-basics-to-advanced-cursor-based-strategies-8cd8f59d92ee
- author_url
- https://medium.com/@mariumgoraya13
- status
- ok
- fetched_at
- 2026-07-18 00:26:52