REST API Pagination: Compare Offset, Cursor, and Keyset Strategies
rest-apipaginationbackenddatabasesapi-designperformance

REST API Pagination: Compare Offset, Cursor, and Keyset Strategies

PPrograma Space Editorial Team
2026-11-08
7 min read

Compare offset, cursor, and keyset pagination, with implementation examples, testing checks, and guidance for changing REST API datasets.

REST API pagination is not just a user-interface concern: the strategy you choose affects database load, response consistency, caching, and how safely clients can move through changing data. This guide compares offset, cursor, and keyset pagination, then gives you a practical checklist for choosing, implementing, testing, and revisiting a pagination design.

Overview

Pagination limits the number of records returned by a listing endpoint. Instead of sending an entire collection, the API returns a bounded page and enough information for the client to request the next or previous portion.

A basic response might include an array of records and navigation metadata:

{
  "data": [
    { "id": 101, "created_at": "2026-10-01T10:00:00Z" },
    { "id": 100, "created_at": "2026-10-01T09:58:00Z" }
  ],
  "page_size": 2,
  "next_cursor": "eyJjcmVhdGVkX2F0Ijoi..."
}

The important design decision is how the server identifies the requested slice. Offset pagination uses a page number or row offset. Cursor pagination returns an opaque token representing a position in the result set. Keyset pagination uses the values of an ordered column, or combination of columns, as the boundary for the next query. Cursor pagination and keyset pagination are closely related, but they are not always identical: a cursor may encode keyset values, a server-side snapshot, or another traversal state.

StrategyPerformanceStable sortingDeep pagesCachingComplexityBackwards compatibility
OffsetSimple, but large offsets may become expensive depending on the database and queryRequires a deterministic sort; records can shift when data changesLess suitable for frequently accessed deep pagesEasy to cache by URL and page parametersLowOften easiest for existing clients
CursorUsually efficient for sequential traversal when implemented with indexed orderingCan provide stable traversal if the cursor and sort rules are designed correctlyWell suited to feeds and large collectionsCache keys are less human-readable and may be request-specificMediumMay require a new response and request contract
KeysetEfficient when boundary columns are indexedStrong for deterministic forward or backward traversalGenerally better than offset for deep navigationCacheable, although URLs contain boundary valuesMediumCan be introduced as a new endpoint or mode

No strategy is automatically best. Choose based on how clients navigate, how often records change, whether users need arbitrary page jumps, and what the database can execute predictably.

Checklist by scenario

Choose offset pagination for page-oriented interfaces

Offset pagination is a reasonable default for administrative tables, reports, and smaller collections where users expect numbered pages or direct access to a page. A request could look like:

GET /orders?limit=25&offset=50

The query conceptually selects 25 rows after the first 50 ordered rows. Always apply an explicit, deterministic ORDER BY. Sorting only by a column that can contain duplicates, such as a status or timestamp, can produce unstable boundaries. Add a unique tie-breaker, commonly an ID:

ORDER BY created_at DESC, id DESC
  • Set a server-side maximum for limit.
  • Normalize invalid, negative, or excessively large offsets.
  • Document whether offsets count records or pages.
  • Decide whether the response includes a total count; counts can add database work and may become stale immediately.
  • Test what happens when records are inserted or deleted between requests.

Choose cursor pagination for feeds and continuously changing collections

Cursor pagination is a strong fit for activity feeds, notifications, message histories, and APIs where clients usually request the next batch rather than jump to page 37. The first request has no cursor:

GET /events?limit=20

The response returns an opaque cursor:

{
  "data": [
    { "id": "evt_200", "occurred_at": "2026-10-02T12:00:00Z" }
  ],
  "next_cursor": "opaque-value",
  "has_more": true
}

The client sends that value back without interpreting it:

GET /events?limit=20&after=opaque-value
  • Encode the ordering boundary, not arbitrary client-controlled SQL or query fragments.
  • Sign or otherwise protect cursors if tampering could expose data or alter query behavior.
  • Return a clear error for malformed, expired, or incompatible cursors.
  • Define whether cursors are reusable and whether they remain valid after deployments or data changes.
  • Provide a documented way to restart from the beginning.

Choose keyset pagination for high-volume ordered data

Keyset pagination queries for records beyond a known sort key instead of asking the database to skip an increasing number of rows. For descending order by creation time and ID, the next-page condition is conceptually:

WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT :limit

Not every database or query builder expresses row-value comparisons in the same way, so use the equivalent grouped condition when necessary:

WHERE created_at < :last_created_at
   OR (created_at = :last_created_at AND id < :last_id)

Create an index that supports the filter and ordering as closely as your database permits. For composite ordering, include every boundary column in the same logical order. Keyset pagination is less convenient when a product requirement demands arbitrary page numbers, but it is often a better fit for sequential traversal through a large, changing dataset.

Use a hybrid contract when clients have different needs

An API can expose offset pagination for a back-office table and cursor pagination for a public feed. Avoid silently changing the meaning of an existing parameter. If a mature endpoint needs a new strategy, introduce an explicit version, mode, or new endpoint and document the migration path.

What to double-check

Ordering and consistency

Pagination is only as reliable as its ordering. Use a stable sort with a unique tie-breaker. If the primary sort value can change while a client is paging, records may move between pages. Decide whether that behavior is acceptable or whether the endpoint needs a snapshot-oriented design.

Database support and query plans

Inspect the actual query shape rather than assuming that an index is being used. Check filters, joins, sort columns, and the size of the selected fields. Large offset queries, expensive total counts, and unbounded limits deserve particular attention. Recheck plans after schema changes, new filters, or a meaningful increase in data volume.

Response and specification details

Document parameter names, defaults, maximum page size, ordering, error responses, and navigation fields in the API contract. If you use OpenAPI or another description format, make the pagination parameters and response schema explicit. State whether an empty result includes a next link, a null cursor, or both.

Security and information exposure

Do not assume that pagination prevents unauthorized access. Apply authorization before selecting the page. Treat cursors as data that may contain sensitive identifiers or query state; opaque encoding is not the same as encryption. Avoid exposing internal database details in error messages.

Client behavior

Tell clients to follow the server's returned navigation information rather than constructing cursors. Clients should handle empty pages, repeated requests, expired cursors, rate limits, and records that change during traversal. For retryable requests, clarify whether repeating a page request is expected to return the same data or merely a valid page.

Common mistakes

  • Using no explicit ordering: Database row order is not a pagination contract. Add a deterministic sort.
  • Sorting by a non-unique column alone: Equal values can cause duplicates or omissions across boundaries. Add a unique tie-breaker.
  • Returning an unbounded page size: A client mistake can become a memory, latency, or database problem. Enforce limits server-side.
  • Calling every token a cursor without defining its behavior: Document whether it is opaque, signed, expiring, reusable, or tied to a particular filter and sort.
  • Assuming cursor pagination automatically fixes consistency: New, deleted, or updated records can still affect traversal unless the ordering and snapshot behavior are designed for that case.
  • Adding a total count to every response: A count may be useful for some interfaces, but it is not free and can be misleading on a changing collection.
  • Testing only the first page: Boundary bugs usually appear on later pages, at duplicate sort values, or when data changes between requests.
  • Changing pagination semantics silently: Existing clients may rely on parameter names, ordering, or metadata. Version the contract or provide a clear transition.

Minimum testing checklist

  • Request the first page, a middle page, and an empty page.
  • Use page sizes of 1, the default, the maximum, zero, a negative value, and an excessively large value.
  • Insert a record between two page requests and check for duplicates or missing records.
  • Delete a record between requests and verify the client receives a valid response.
  • Create many records with identical primary sort values.
  • Reuse a cursor, alter its filter, and submit a malformed or expired cursor.
  • Verify forward and backward navigation if both directions are supported.
  • Compare query latency and database plans at shallow and deep traversal points.

When to revisit

Revisit the pagination design before a new endpoint becomes public, when a collection grows substantially, or when clients report duplicates, missing records, slow deep pages, or inconsistent navigation. Also review it when the sort requirements change, a new filter or join is introduced, the database schema is reorganized, or the framework and API-description conventions are upgraded.

Before each review, capture representative queries, response sizes, common page depths, and the expected client navigation pattern. Then work through this short action list:

  1. Write down the endpoint's ordering, page-size limits, and consistency expectations.
  2. Run the duplicate, omission, invalid-cursor, and changing-data tests against realistic fixtures.
  3. Inspect database plans for both the first request and a deep traversal.
  4. Check that documentation and generated client types match the actual response.
  5. Record any contract change and provide a migration path for existing consumers.

For practical API debugging, a JSON formatter for API responses can make page metadata easier to compare, while the SQL formatter guide is useful when reviewing generated pagination queries. Keep this checklist with the endpoint design notes so it can be reused whenever the data, database, or client workflow changes.

Related Topics

#rest-api#pagination#backend#databases#api-design#performance
P

Programa Space Editorial Team

Technical Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.