GraphQL Pagination Patterns
Pagination breaks large lists into smaller chunks. Returning 10,000 products in one query would crash the browser and overload the database. Pagination returns a manageable slice of data and gives the client a way to ask for the next slice.
Three Pagination Strategies
Strategy How it works Best for ──────── ──────────── ──────── Offset-based Skip N items, take M Simple admin tables Cursor-based Start after item X Infinite scroll, feeds Page-based Return page number N Traditional page buttons
Offset Pagination (Limit + Offset)
Offset pagination uses two arguments: limit (how many items to return) and offset (how many items to skip). It maps directly to SQL's LIMIT and OFFSET.
Schema:
────────
type Query {
products(limit: Int!, offset: Int!): [Product!]!
}
Page 1 — first 10: Page 2 — next 10:
─────────────────── ──────────────────
{ products( { products(
limit: 10, offset: 0 limit: 10, offset: 10
) { name price } } ) { name price } }
Visual:
────────
Products: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,...]
└────────────┘ offset:0 limit:10
└────────────┘ offset:10 limit:10
Offset pagination is simple to implement but has a flaw: if items are added or deleted between page requests, items can appear twice or be skipped. A new item at position 1 pushes everything down, so page 2 now starts one item earlier than the client expects.
Page-Number Pagination
Page-number pagination is a friendlier version of offset pagination. The client sends a page number instead of calculating offsets. The server computes the offset internally.
Schema:
────────
type PaginatedProducts {
items: [Product!]!
totalItems: Int!
totalPages: Int!
currentPage: Int!
}
type Query {
products(page: Int = 1, perPage: Int = 20): PaginatedProducts!
}
Query:
───────
{
products(page: 2, perPage: 10) {
items { name price }
totalItems
totalPages
currentPage
}
}
Response:
──────────
{
"products": {
"items": [{ "name": "Mouse" }, ...],
"totalItems": 247,
"totalPages": 25,
"currentPage": 2
}
}
When to Use Offset vs Cursor Pagination
Offset / Page-number: Cursor-based: ───────────────────── ───────────── Data does not change often Real-time or frequently updated data Users navigate to specific pages Infinite scroll / "Load more" button SQL database with simple queries Consistent results despite insertions Admin dashboards Social media feeds, chat history Small to medium datasets Very large datasets
Key Points
- Never return unbounded lists in production — always paginate.
- Offset pagination is simple but can miss or duplicate items when data changes between requests.
- Page-number pagination is a client-friendly wrapper around offset pagination.
- Cursor-based pagination (covered in the next topic) solves the consistency problem for live data.
- Return metadata (totalItems, totalPages) alongside the items so the UI can render controls correctly.
