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

REST API Pagination URL Design: Offset, Cursor, and Link Headers

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

October 28, 20268 min read5 topics
REST API Pagination URL Design: Offset, Cursor, and Link Headers
Explore the details
#REST API#API design#pagination#HTTP#backend development
October 28, 20268 min read

Pagination URLs shape how clients move through a collection—and how reliably they do it when that collection changes. This guide compares offset and cursor pagination, explains how to represent page size and continuation state, and shows how HTTP Link headers can make navigation easier for API clients.

Key Takeaways

  • Offset pagination is straightforward and supports jumping to a numbered position, but deep offsets can be costly and changes to the dataset can cause duplicates or omissions between requests.
  • Cursor pagination is often better for sequentially traversing large or changing datasets, but its tokens and navigation semantics require careful design.
  • Parameter names such as limit, page_size, offset, and after are conventions, not universal requirements. Document your API’s choices.
  • A Link header with relations such as next lets clients follow server-generated URLs instead of reconstructing continuation state.
  • Validate page sizes and cursor tokens. Treat cursors as potentially sensitive data, not as safe merely because they are encoded.

Choose pagination based on how clients navigate

Pagination splits a collection response into manageable portions. The right design depends on whether clients need to jump to a position, steadily traverse changing data, or both.

ApproachContinuation exampleUseful whenMain trade-off
Offset?limit=50&offset=100Clients need simple numbered pages or arbitrary offsetsDeep offsets may be expensive; inserts and deletes can shift results
Cursor?limit=50&after=opaque-tokenClients mostly move forward through large or frequently changing collectionsClients cannot usually jump to an arbitrary page; tokens need lifecycle and validation rules

Neither approach guarantees a consistent snapshot by itself. If clients need every record from a fixed point in time, define snapshot semantics separately—for example, with a snapshot identifier or a documented export workflow.

Offset pagination: simple, but sensitive to dataset changes

An offset says how many matching records to skip. A request such as GET /v1/orders?limit=50&offset=100 asks for up to 50 records after the first 100 in the chosen ordering.

This is easy to explain and can support page-number interfaces. It also makes direct jumps possible: a client can request offset 500 without first retrieving earlier pages.

There are two important costs:

  • Deep offsets can require more work. Depending on the database and query plan, the system may still need to walk past many matching rows before returning the requested page. Measure your own query patterns rather than assuming a particular cost.
  • Results can shift between requests. Suppose a client retrieves records at offsets 0–49, then a new record is inserted at the beginning. The next request at offset 50 may repeat a record from the previous response. A deletion near the beginning can instead cause a record to be skipped.

Use a deterministic ordering, preferably with a unique tie-breaker. For example, ordering only by created_at is ambiguous when multiple orders share a timestamp; ordering by created_at and then id makes the position unambiguous. A stable sort helps, but it does not prevent shifts caused by changes to the collection.

Offset pagination is a reasonable choice for small or relatively stable collections, and for interfaces that genuinely require numbered pages. If records change frequently or clients need to traverse many results, consider a cursor.

Cursor pagination: continue from a position

A cursor identifies a continuation position in an ordered result set. A request might look like this:

GET /v1/orders?limit=50&after=eyJjcmVhdGVkX2F0IjoiLi4uIn0

The token could represent the last item’s sort values, an opaque server-side lookup key, or a snapshot position. The client should treat it as an opaque value and return it unchanged.

A common implementation uses keyset pagination. If results are sorted by created_at and id, the next page can select records after the last pair from the previous page. The unique tie-breaker matters: without it, records sharing a timestamp may be missed or repeated.

Cursor pagination can avoid walking through a large offset and is usually a better fit for sequential traversal. But it has trade-offs:

  • Clients generally cannot jump to an arbitrary page number.
  • A cursor is only meaningful for the query and ordering that created it. Changing filters, sort order, tenant, or authorization context may make it invalid or unsafe to reuse.
  • A cursor over a live dataset does not automatically provide a frozen snapshot. New or updated records may still affect what a client sees, depending on the ordering and consistency model.
  • Backward navigation requires an explicit design, such as a before cursor or a previous-page link. Do not imply that a next-page cursor automatically supports reverse traversal.

Document whether cursors expire, whether they can be reused, and what clients should do when a cursor is invalid or expired. A clear client-facing error is better than silently returning an unrelated page.

Choose query parameter names deliberately

There is no single required naming scheme for pagination parameters. Common choices include:

  • Page size: limit, page_size, or per_page.
  • Offset position: offset or a page number such as page.
  • Cursor continuation: after and before, or a single cursor parameter.

Pick names that match the meaning you support, then use them consistently across endpoints. If page means a one-based page number, document that; if it means an offset, do not leave clients to guess. State whether page size is optional, its default, its maximum, and what happens when a client requests a value outside the allowed range.

For example, an API could document limit as a positive integer with a default of 25 and a maximum of 100. It should decide whether values above 100 are rejected or capped, and apply that behavior consistently. Reject malformed, negative, or nonsensical values with a useful client error rather than allowing them to reach database logic unchecked.

Use Link headers to expose navigation

An API can put a continuation URL in its JSON response, an HTTP Link header, or both. A Link header uses a URL and a relation that describes how the link relates to the current response. For pagination, next, prev, first, and last are useful relation names where the corresponding navigation is available.

For example:

HTTP/1.1 200 OK
Content-Type: application/json
Link: <https://api.example.com/v1/orders?limit=50&after=opaque-token>; rel="next"
Link: <https://api.example.com/v1/orders?limit=50&before=previous-token>; rel="prev"

A JSON response might also include page data and a count:

{
  "data": [
    { "id": "ord_101", "status": "paid" },
    { "id": "ord_102", "status": "pending" }
  ],
  "total_count": 248
}

The example shows the response shape, not a requirement to include total_count. Computing an exact total can add cost, and the value can become stale as the collection changes. If you omit it, clients can follow the next link until there is no next page.

Generate links from the API’s canonical public origin and preserve applicable filters and sort parameters. Clients should follow the URL supplied by the server rather than reconstructing a cursor URL themselves. This gives the server room to change token formats and pagination internals without requiring clients to understand them.

Protect and validate continuation state

A cursor may contain identifiers, timestamps, sort values, or other details about the underlying data. Base64 encoding only changes the representation; it does not make the contents secret. Signing a token can help detect tampering, but a signature alone does not hide its contents.

Choose a token design that fits the data and threat model:

  • Use an opaque random identifier that resolves to server-side state when you need to keep cursor contents out of the URL.
  • If a self-contained token is appropriate, include only the minimum state required. Use authenticated encryption when its contents must be confidential, and validate the token before using its values.
  • Bind continuation state to the relevant endpoint, filters, ordering, and authorization scope. Re-check authorization on every request; possession of a cursor must not grant access.
  • Set reasonable expiration and size limits. Return a clear error for malformed, expired, or incompatible cursors.
  • Avoid putting credentials or unrelated sensitive information in query parameters. URLs may be recorded in logs, browser history, analytics, and intermediary systems.

Treat query values as untrusted input. Parse page sizes as bounded integers, validate cursor structure and signatures where applicable, and use parameterized database queries. A cursor should not let a client inject raw SQL, choose an unrestricted sort expression, or bypass tenant filters.

Common Mistakes

  • Assuming offset results are stable: Inserts and deletes can shift positions even if the sort order is deterministic.
  • Sorting on a non-unique field: Add a unique tie-breaker to make continuation positions unambiguous.
  • Calling a token encrypted when it is only encoded: Encoding and signing do not provide confidentiality.
  • Letting clients build continuation URLs: Return complete next and previous links when available, and document that clients should follow them.
  • Leaving limits undefined: Specify defaults, maximums, and behavior for invalid or excessive values.
  • Promising snapshot consistency without implementing it: Cursor pagination alone does not freeze a changing dataset.
  • Exposing exact totals by default: Decide whether the count is useful enough to justify its cost and freshness limitations.

Best Practices

  1. Choose offset pagination when page-number jumps matter and the collection size and query costs make it appropriate. Choose cursors for efficient sequential traversal of large or changing collections.
  2. Define a stable ordering with a unique tie-breaker before designing continuation tokens.
  3. Document parameter names, defaults, maximum page size, ordering, cursor expiration, and error behavior.
  4. Return complete navigation URLs, commonly through Link headers, and keep clients from depending on token internals.
  5. Treat tokens and all query parameters as untrusted. Limit their size, validate their scope, and continue enforcing authorization.
  6. Decide explicitly whether the API promises a live view or a consistent snapshot.

Conclusion

Good REST API pagination URL design makes continuation predictable for clients without hiding important limitations. Use offsets when direct page access is valuable; use cursors when sequential traversal and changing datasets make offsets a poor fit. In either case, define ordering and limits, expose navigation clearly, and handle cursor data as untrusted state.

References

I could not retrieve or verify authoritative source documents in this environment. The HTTP Link header guidance above should be checked against the current Web Linking specification before publication; no live source consultation is claimed here.