{"post":{"slug":"cursor-pagination-explained-2026","title":"Cursor Pagination: Why Offset Breaks at Scale","excerpt":"Offset pagination gets slower with every page and skips rows when data changes. Cursor pagination fixes both. Here is how to implement it correctly.","category":"code","date":"July 24, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">O</span>ffset 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.\n\nCursor pagination avoids both by remembering where you were rather than counting how far in you are.\n\n## What changed in 2026\n\n- **Cursor pagination became the API default.** New APIs adopting cursors rather than page numbers moved from a best practice to a common expectation.\n- **Opaque cursor encoding standardized.** Encoding the cursor rather than exposing raw column values became normal, preserving the ability to change implementation.\n- **Tiebreaker requirements got documented.** The subtle bug from paginating on a non-unique column became well enough understood to appear in guidance.\n- **Hybrid approaches spread.** Offering cursors for programmatic access alongside limited page numbers for human browsing became common.\n\n## How they compare\n\n| | Offset pagination | Cursor pagination |\n| --- | --- | --- |\n| Performance at depth | Degrades linearly | Constant |\n| Stability under inserts and deletes | Rows skip and duplicate | Stable |\n| Jump to an arbitrary page | Yes | No |\n| Total page count | Available | Requires a separate count |\n| Implementation complexity | Trivial | Moderate |\n| Index requirement | Any | Must match the sort order |\n\nThe 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.\n\n## Implementing it correctly\n\nThe cursor encodes the sort position of the last row returned. The next query asks for rows sorted the same way, positioned after that value.\n\nThe 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.\n\nThe 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.\n\nEncode 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.\n\nReturn 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](/blog/api-error-design-2026).\n\n## Common mistakes\n\n- **No unique tiebreaker.** Silently skips or duplicates rows.\n- **Index not matching the sort order.** Performance problem returns.\n- **Exposing raw cursor values.** Clients depend on the format; you cannot change it.\n- **Allowing a cursor with different sort parameters.** Produces incoherent results.\n- **Exact total counts on large tables.** Expensive, and usually not needed.\n- **No indicator of more results.** Forces an extra empty request.\n\n## FAQ\n\n**Can I paginate backwards?**\nYes, by reversing the comparison and the sort, then reversing the result. Both directions should be supported.\n\n**What about sorting by a mutable column?**\nA 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.\n\n**How do I offer page numbers as well?**\nHybrid: cursors for the main path, with limited offset support for shallow pages where the cost is acceptable.\n\n**Does this apply to non-relational stores?**\nYes, and many partitioned stores provide native continuation tokens that work the same way — see [partition key design](/blog/partition-key-design-2026).\n\n## Where to go next\n\nFor API conventions, read [API error design](/blog/api-error-design-2026) and [API versioning strategies](/blog/api-versioning-strategies-2026). For protecting endpoints under load, [rate limit algorithms](/blog/rate-limit-algorithms-2026).","hue":218,"metaTitle":"Cursor Pagination Explained: Beyond Offset 2026","metaDescription":"Why offset pagination degrades and loses rows, how cursor pagination works, why a unique tiebreaker matters, and the tradeoffs against page numbers.","keywords":["cursor pagination explained","keyset pagination","offset pagination problems","stable pagination ordering","api pagination design","deep pagination performance","opaque cursor tokens","database query optimization","api design 2026"],"tldr":["**Offset pagination degrades linearly** — the database counts and discards every skipped row.","**It also skips and duplicates rows** — when data changes between page requests, offsets shift.","**Cursor pagination encodes a position instead** — constant-time regardless of depth.","**The cursor must include a tiebreaker** — a non-unique sort column produces skipped rows without one.","**Skip:** cursor pagination when users need to jump to page fifty. Cursors are sequential by nature."],"readTime":"4 min read","wordCount":780},"related":[{"slug":"pagination-explained-2026","title":"Pagination explained in 2026: offset, cursor, and keyset compared","excerpt":"Pagination shapes API performance and user experience more than most engineers expect. Here is the honest 2026 guide to offset, cursor, and keyset pagination.","category":"code","date":"March 2, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":311},{"slug":"how-to-paginate-an-api-2026","title":"How to paginate an API in 2026: offset, cursor, and keyset compared","excerpt":"Pagination done wrong causes slow queries, inconsistent results, and thundering herds. This is the 2026 guide to offset, cursor, and keyset pagination.","category":"code","date":"April 16, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":316},{"slug":"pagination-cursor-vs-offset-2026","title":"Pagination in 2026: cursor vs offset — which one to use","excerpt":"Offset pagination breaks on large datasets and concurrent writes. Cursor pagination is the 2026 default for APIs and feeds — here is exactly how to implement it.","category":"code","date":"March 2, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":311}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"crdt-explained-2026","title":"CRDTs Explained: Merging Edits Without a Server Deciding","excerpt":"Conflict-free replicated data types let concurrent edits merge automatically into the same result. Here is how they work and what they cost.","category":"code","date":"July 24, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},"older":{"slug":"data-poisoning-explained-2026","title":"Data Poisoning Explained: Corrupting a Model at the Source","excerpt":"Poisoning attacks manipulate training data to plant behaviour that survives into the model. Here is how it works and what actually defends against it.","category":"ai","date":"July 24, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":6}}