Offset pagination is the obvious implementation and it has two problems that both get worse with scale. Requesting page five hundred means the database must find and discard the first ten thousand rows before returning anything, so response time grows linearly with page depth. And if a row is inserted or deleted between requests, every subsequent offset shifts — users see a row twice or miss one entirely.
Cursor pagination avoids both by remembering where you were rather than counting how far in you are.
What changed in 2026
- Cursor pagination became the API default. New APIs adopting cursors rather than page numbers moved from a best practice to a common expectation.
- Opaque cursor encoding standardized. Encoding the cursor rather than exposing raw column values became normal, preserving the ability to change implementation.
- Tiebreaker requirements got documented. The subtle bug from paginating on a non-unique column became well enough understood to appear in guidance.
- Hybrid approaches spread. Offering cursors for programmatic access alongside limited page numbers for human browsing became common.
How they compare
|
Offset pagination |
Cursor pagination |
| Performance at depth |
Degrades linearly |
Constant |
| Stability under inserts and deletes |
Rows skip and duplicate |
Stable |
| Jump to an arbitrary page |
Yes |
No |
| Total page count |
Available |
Requires a separate count |
| Implementation complexity |
Trivial |
Moderate |
| Index requirement |
Any |
Must match the sort order |
The jump-to-page row is the genuine tradeoff. Cursors are inherently sequential — you can go forward and back from where you are, and you cannot construct a cursor for page fifty without traversing there. For an API consumed by programs, that is almost never needed. For a human browsing a results table, it sometimes is, which is why hybrid approaches exist.
Implementing it correctly
The cursor encodes the sort position of the last row returned. The next query asks for rows sorted the same way, positioned after that value.
The subtle requirement is a unique tiebreaker. If you sort by creation time and several rows share a timestamp, a cursor holding only that timestamp cannot distinguish which of them you already returned — so the next page either skips some or repeats them. Including a unique column, typically the identifier, as a secondary sort and in the cursor removes the ambiguity.
The index must match the sort. A composite index on the sort columns in the same order is what makes the positioned lookup fast; without it the database scans, and you have the offset problem back in different clothing.
Encode the cursor opaquely rather than exposing raw values. Clients that parse and construct cursors will break when you change the sort or add a tiebreaker, and an encoded token communicates that the contents are not part of the contract. Include enough to validate that a cursor matches the current query parameters, so a client cannot reuse a cursor from a differently-sorted request.
Return whether more results exist rather than requiring the client to request an empty page to find out. And handle the total-count question deliberately — an exact count over a large table is expensive, and an approximate count or none at all is frequently the right answer. The error semantics for all of this belong in your general API error design.
Common mistakes
- No unique tiebreaker. Silently skips or duplicates rows.
- Index not matching the sort order. Performance problem returns.
- Exposing raw cursor values. Clients depend on the format; you cannot change it.
- Allowing a cursor with different sort parameters. Produces incoherent results.
- Exact total counts on large tables. Expensive, and usually not needed.
- No indicator of more results. Forces an extra empty request.
FAQ
Can I paginate backwards?
Yes, by reversing the comparison and the sort, then reversing the result. Both directions should be supported.
What about sorting by a mutable column?
A row whose sort value changes between requests can move relative to the cursor and be skipped or repeated. Prefer immutable sort columns, or accept the imperfection.
How do I offer page numbers as well?
Hybrid: cursors for the main path, with limited offset support for shallow pages where the cost is acceptable.
Does this apply to non-relational stores?
Yes, and many partitioned stores provide native continuation tokens that work the same way — see partition key design.
Where to go next
For API conventions, read API error design and API versioning strategies. For protecting endpoints under load, rate limit algorithms.