How to Paginate API Results: Best Practices
Pagination is essential for APIs that return large datasets. Without it, a single request can return thousands of records, causing slow responses, high memory usage, and poor user experience. This guide covers how to paginate API results using modern best practices, comparing offset and cursor-based approaches, and showing real-world implementation patterns.
![API pagination architecture diagram showing client requests with page parameters]
Why Pagination Matters
| Problem Without Pagination | Impact | |---------------------------|--------| | Large response payloads | Slow network transfer, high latency | | Memory spikes on server | Out-of-memory errors under load | | Database timeouts | Full table scans block other queries | | Poor mobile experience | Long loading times, app crashes | | Unbounded costs | Cloud egress and compute bills surge |
A well-designed pagination strategy keeps APIs fast, predictable, and scalable.
Offset vs Cursor Pagination
The two dominant pagination patterns are offset-based and cursor-based. Each has distinct tradeoffs.
| Aspect | Offset Pagination | Cursor Pagination | |--------|-------------------|-------------------| | Mechanism | Skip N records, take M | Use opaque cursor from last item | | Consistency | Can miss or duplicate rows | Stable, consistent ordering | | Database cost | OFFSET scans all skipped rows | Index seek on cursor column | | Deep page performance | Slows dramatically at high offsets | Constant time per page | | Complexity | Simple, widely understood | Requires cursor encoding | | Use case | Admin panels, static catalogs | Feeds, real-time apps |
Rule of thumb: Use offset pagination for small, mostly static datasets. Use cursor pagination for large, frequently updated datasets or infinite scroll.
REST API Pagination Patterns
Offset Pagination with Limit
The simplest approach passes offset and limit query parameters.
``
GET /api/orders?offset=40&limit=20
``
Pros: Easy to implement, familiar to clients, supports random page access. Cons: Database must scan and discard skipped rows, rows can shift between requests.
Cursor Pagination with bookmarks
Return a next_cursor opaque token in the response. Clients send it back to fetch the next page.
``
GET /api/feed?cursor=eyJpZCI6MTIzfQ&limit=20
``
Pros: Constant query cost, no duplicates or misses, ideal for real-time feeds. Cons: No random page access, cursor usually encodes internal state.
Keyset Pagination
Use explicit column values instead of an opaque cursor.
``
GET /api/orders?last_id=1024&limit=20
``
Pros: Transparent, cacheable, index-friendly. Cons: Requires a stable unique ordering column.
GraphQL Pagination Patterns
Relay-Style Cursor Connections
GraphQL community standards recommend Relay-style cursor connections.
``graphql
query {
products(first: 20, after: "opaqueCursor") {
edges { node { id name } cursor }
pageInfo { endCursor hasNextPage }
}
}
``
This pattern bundles edges, cursors, and navigation metadata into a single response.
Offset-Based in GraphQL
Simpler but less common in production GraphQL APIs.
``graphql
query {
users(offset: 40, limit: 20) {
id name
}
}
``
Modern Best Practices for Pagination
| Best Practice | Why It Matters | Implementation |
|---------------|---------------|-----------------|
| Always set a max page size | Prevent abuse and runaway queries | Cap limit at 100-1000 |
| Return total count carefully | Counting large tables is expensive | Use estimated counts or omit totals |
| Include navigation links | Clients should not construct URLs | next, prev, first, last in headers or body |
| Sort deterministically | Order matters for cursor stability | Always sort by unique column(s) |
| Document cursor expiry | Cursors may invalidate over time | Define TTL and refresh strategy |
| Cache paginated responses | Reduce database load | Cache at CDN or app layer |
| Support both formats | Different consumers need different patterns | Offer offset and cursor endpoints |
Response Envelope Example
A modern response envelope gives clients everything they need to navigate.
``json
{
"data": [...],
"pagination": {
"limit": 20,
"offset": 40,
"total": 542,
"next_cursor": "eyJpZCI6NDB9",
"prev_cursor": "eyJpZCI6MjB9"
}
}
``
For cursor pagination, omit total if counting is expensive, and rely on hasNextPage.
Database Query Patterns
Efficient pagination requires the right query shape.
| Pattern | Query Example | Performance | |---------|---------------|-------------| | Offset | SELECT * FROM posts ORDER BY id LIMIT 20 OFFSET 40 | Slows at high offset | | Keyset | SELECT * FROM posts WHERE id > 1024 ORDER BY id LIMIT 20 | Constant time | | Cursor seek | SELECT * FROM posts WHERE created_at > '...' ORDER BY id LIMIT 20 | Constant time |
Add covering indexes on ordering columns. Avoid SELECT * in paginated endpoints; project only needed fields.
Infinite Scroll vs Pagination UI
Choose the right UX pattern for your product.
| UI Pattern | Best For | SEO Impact | |------------|----------|------------| | Traditional pagination | Blogs, documentation, search results | High: each page is crawlable | | Infinite scroll | Social feeds, image galleries | Low: requires special handling | | Load more button | E-commerce, moderate feeds | Medium: partial crawlability |
For SEO-critical content, traditional pagination with rel="next" and rel="prev" links ensures search engines discover all pages.
Common Pagination Mistakes
| Mistake | Consequence | Fix |
|---------|-------------|-----|
| No max page size | Abuse, DB overload | Enforce hard limit on limit |
| Counting total on every request | Slow queries, timeouts | Cache counts, use估算 counts |
| Inconsistent sorting | Duplicate or missing items | Always sort by unique column first |
| Exposing internal IDs in cursors | Security risk, coupling | Sign or encode cursors |
| Ignoring timezone in date cursors | Wrong results for global users | Store timestamps in UTC |
Conclusion
Effective pagination balances client experience, database performance, and API security. Start with cursor pagination for new APIs handling large or dynamic datasets. Add offset pagination only when random page access is a hard requirement. Always document limits, enforce maximum page sizes, and monitor query performance in production.