TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

Cursor vs. Offset Pagination: Choosing Stable API URLs

Practical insights, clear explanations, and useful ideas for understanding the web.

October 28, 20268 min read6 topics
Cursor vs. Offset Pagination: Choosing Stable API URLs
Explore the details
#API design#pagination#REST APIs#cursor pagination#offset pagination#data consistency
October 28, 20268 min read

Offset pagination is easy to understand, but changing data can make page numbers unreliable. Cursor pagination can keep a client moving through an ordered dataset with fewer position shifts, but it adds implementation and client-handling trade-offs. Here’s how to choose between them and design pagination URLs that clients can follow safely.

Key Takeaways

  • Offset pagination uses a numeric position, such as offset=20. It is simple and supports jumping to a page, but inserts or deletions can cause duplicates or skipped records between requests.
  • Cursor pagination uses a token tied to an ordered position. It generally handles changes before that position more predictably, but requires a deterministic sort and gives clients less freedom to jump to an arbitrary page.
  • Neither approach automatically provides a consistent snapshot of changing data. Use snapshot or version semantics when a multi-page export must represent one point in time.
  • Return a next URL or token in each response, and document token lifetime, ordering, filters, and page-size behavior.

How the two approaches work

Offset pagination identifies a page by how many records to skip. For example, with a page size of three:

GET /v1/orders?limit=3&offset=0

A response might include:

{
  "data": [{"id": "A"}, {"id": "B"}, {"id": "C"}],
  "links": {
    "next": "https://api.example.com/v1/orders?limit=3&offset=3"
  }
}

The next request starts at position three. The server can also expose a total count or let clients calculate an offset for a particular page. That makes offset pagination convenient for small or relatively static collections and interfaces with page numbers.

Cursor pagination identifies a position using a token rather than a count of skipped records:

GET /v1/orders?limit=3

A response might return an opaque token and a complete next URL:

{
  "data": [{"id": "A"}, {"id": "B"}, {"id": "C"}],
  "next_page_token": "eyJvcmRlciI6ImV4YW1wbGUifQ",
  "links": {
    "next": "https://api.example.com/v1/orders?limit=3&page_token=eyJvcmRlciI6ImV4YW1wbGUifQ"
  }
}

The token here is illustrative. In a real API, clients should treat it as opaque: store it and send it back without decoding or constructing one themselves. The server uses it to continue from a position in a defined ordering. Google’s API guidance recommends opaque page tokens and a next_page_token pattern; the exact response shape remains an API design choice.

Why changing data affects offset pages

Suppose the ordered records are A, B, C, D, E, F. A client requests the first three records and receives A, B, C. Before it requests offset=3, a new record X is inserted at the beginning:

Before insert: X, A, B, C, D, E, F
Request offset=3: C, D, E

The client sees C twice. The offset still means “skip three records,” not “continue after C.”

Deletion can cause the opposite problem. If A is removed after the first response, the collection becomes B, C, D, E, F. The request for offset=3 returns E, F, so D is skipped. Changes elsewhere in the collection can shift the numeric position even though the client has not changed its request parameters.

This behavior is not necessarily a defect. It may be acceptable when the collection changes rarely, the client can tolerate occasional repeats, or the product needs direct access to numbered pages. It is risky when clients assume that consecutive requests form a gap-free traversal.

How cursors reduce position shifts

A cursor should refer to a position in a deterministic ordering, not merely to a mutable row number. For example, order orders by created_at and then by a unique id to break ties:

ORDER BY created_at ASC, id ASC

The server can encode the last returned pair in an opaque token. Internally, the next query is conceptually:

WHERE (created_at, id) > (:last_created_at, :last_id)
ORDER BY created_at ASC, id ASC
LIMIT :limit

That tuple comparison is illustrative; exact SQL support and query plans vary by database. The unique tie-breaker matters: timestamps alone may not uniquely order records, so records with the same timestamp could otherwise be repeated or missed.

A cursor-based response might link to the next request as follows:

GET /v1/orders?limit=3&page_token=eyJvcmRlciI6ImV4YW1wbGUifQ

An insert before the cursor’s position will not shift the continuation point the way it shifts an offset. But cursor pagination is not a snapshot by itself. An item inserted after that position may appear on a later page; deleting an item means it will not be returned; and changing an item’s sort key can move it across the cursor boundary. If a client needs a complete, consistent export as of a particular moment, provide a snapshot, version, or documented watermark mechanism rather than promising that a cursor alone freezes the dataset.

Trade-offs: complexity, performance, and usability

ConsiderationOffsetCursor
ImplementationUsually straightforward; requires a stable order for predictable resultsRequires token creation and validation, deterministic ordering, and continuation logic
Changing dataInserts and deletions can shift page boundariesLess affected by changes before the cursor, but not a snapshot guarantee
Deep traversalLarge offsets may be costly because a database may still need to walk past skipped rows; actual cost depends on the database and query planCan be efficient when the ordering and continuation predicate use suitable indexes; measure the real query
Client navigationEasy to request a specific offset or pageBest suited to sequential next/previous traversal; arbitrary page jumps are generally unavailable
CountsOften paired naturally with total counts, though calculating them can itself be expensiveCan avoid requiring a total count; clients can continue until no next token is returned

Choose offset when direct page-number navigation is important, the dataset is modest or stable, and the operational cost is acceptable. Choose cursor pagination for large or frequently changing collections where clients mainly traverse forward or backward. A hybrid API is possible, but supporting two models increases documentation, testing, and maintenance work.

Design pagination URLs clients can rely on

Whichever model you choose, make the ordering and continuation contract explicit:

  1. Use a deterministic sort. Include a unique tie-breaker, and document the default and allowed sort orders.
  2. Return the next request. A complete next URL or a token in the response helps clients avoid rebuilding continuation parameters. The Web Linking standard defines the Link header and link relations, including next; a JSON links object is another common response design.
  3. Bind tokens to the query context. A token should not accidentally continue a different filter or sort. Validate that it belongs to the relevant collection and request context. Protect token integrity if clients must not be able to alter its contents.
  4. Define token and limit behavior. Document whether tokens expire, what happens when they are invalid, whether clients can change limit while continuing, and how the final page is represented. Do not make clients depend on undocumented token internals.
  5. Preserve filters and authorization. Continuation requests must apply the intended filters and the caller’s current authorization checks. A token is not a substitute for access control.
  6. Plan for URL handling. Tokens in URLs can appear in logs, browser history, and monitoring systems. Avoid placing secrets or sensitive personal data in them; use opaque, non-sensitive tokens and apply appropriate logging controls.

For example, an API can return a Link header like this, or put equivalent URLs in its JSON response:

Link: <https://api.example.com/v1/orders?limit=3&page_token=eyJvcmRlciI6ImV4YW1wbGUifQ>; rel="next"

Clients should follow the supplied URL or resend the supplied token according to the API documentation. They should not infer that the token is a permanent bookmark unless the API explicitly guarantees that behavior.

Common Mistakes

  • Using a non-unique sort key. Equal timestamps or names need a unique tie-breaker for reliable continuation.
  • Calling cursors snapshot pagination. A cursor provides a continuation position; concurrent updates can still affect what later pages contain.
  • Exposing cursor internals as a client contract. Changing an encoded token format can break clients that decode or construct tokens.
  • Assuming every offset query is slow or every cursor query is fast. Cost depends on database, indexes, filters, and query plan. Inspect representative queries at realistic depths.
  • Dropping filters from the next URL. A continuation that loses a filter can return records from a broader collection than the client intended.
  • Treating a token as authorization. Recheck permissions on every request, even when the token is valid.

Best Practices

  • Pick the pagination model based on the collection’s update rate, size, navigation needs, and consistency requirements—not fashion.
  • Keep sort order explicit and deterministic, with a unique final key.
  • Test traversal while inserting, deleting, and updating records around page boundaries. State whether duplicates or omissions are possible under concurrent changes.
  • For exports requiring a point-in-time view, define snapshot or watermark semantics separately from pagination.
  • Return a clear terminal state, such as no next link or an empty next-page token, and document error behavior for expired or malformed tokens.
  • Monitor query latency and database plans for realistic filters and deep traversal before making performance claims.

Conclusion

Offset pagination is simpler and supports page jumps, but shifting records can make successive pages repeat or skip results. Cursor pagination is often a better fit for sequential traversal of large, changing collections when it uses a stable order, but it adds token and consistency design work. Choose according to the client’s navigation needs and the guarantees your data can actually provide.

References