Ctrl + K
API19 min read

Pagination in REST APIs

A practical guide to REST API pagination, including page-based, offset, cursor, and keyset pagination, response metadata, sorting, performance, and implementation considerations.

Published: 2026-10-05

REST APIs often work with collections that contain far more records than a client should retrieve in a single response. A users endpoint might contain thousands of accounts, a products endpoint might contain hundreds of thousands of products, and an activity feed could grow continuously. Returning the entire collection for every request wastes bandwidth, increases response time, and places unnecessary work on both the server and client.

Pagination solves this problem by dividing a large collection into smaller portions. Instead of requesting every record at once, the client asks for a limited subset and then requests additional results when needed.

There are several ways to implement pagination. Page numbers and offset-based pagination are simple and familiar, while cursor and keyset pagination can provide more predictable behavior for large or frequently changing datasets. The right approach depends on the size of the collection, how frequently it changes, and what clients need to do with the results.

What Is Pagination?

Pagination is the process of returning a collection in smaller groups instead of returning every item in a single response. The client specifies how many records it wants and, depending on the pagination strategy, which portion of the collection it wants.

GET /api/products?limit=20&offset=0

The server might return the first 20 products. The client can then request the next portion by changing the offset or another pagination parameter.

{
  "data": [
    {
      "id": 1,
      "name": "Keyboard"
    },
    {
      "id": 2,
      "name": "Mouse"
    }
  ],
  "pagination": {
    "limit": 20,
    "offset": 0
  }
}

Why REST APIs Need Pagination

Without pagination, a collection endpoint may eventually return an unnecessarily large response. Even if the endpoint performs well with a few hundred records, the same design can become expensive as the dataset grows.

  • Large responses consume more network bandwidth.
  • The server must read and serialize more records.
  • Clients need more memory to store the response.
  • Large JSON responses take longer to parse.
  • Slow responses can increase request timeouts.
  • Mobile and low-bandwidth clients are affected more strongly.
  • Database queries can become unnecessarily expensive.

Pagination also improves the user experience. A frontend can display the first results quickly instead of waiting for an entire collection to be transferred before anything can be shown.

Basic Pagination Parameters

A simple pagination API commonly exposes parameters such as limit, offset, page, or cursor. The exact names are not standardized, so the API should document them clearly.

ParameterPurpose
limitMaximum number of records returned
offsetNumber of records skipped
pageRequested page number
pageSizeNumber of records requested per page
cursorPosition used to continue from a previous result

It is useful to distinguish between the number of records requested and the position from which records are retrieved. A limit or pageSize controls the amount of data, while offset, page, or cursor identifies the requested portion of the collection.

Page-Based Pagination

Page-based pagination uses a page number and usually a page size. For example, a client can request page 1 with 20 records per page, followed by page 2 and page 3.

GET /api/products?page=1&pageSize=20
GET /api/products?page=2&pageSize=20
GET /api/products?page=3&pageSize=20

The server translates the page number into an internal offset. With a page size of 20, page 1 starts at record 0, page 2 starts at record 20, and page 3 starts at record 40.

Page-based pagination is intuitive for user interfaces that display numbered pages. It is less convenient for continuously changing feeds where records can be inserted or removed between requests.

Offset-Based Pagination

Offset pagination explicitly specifies how many records should be skipped before returning results.

GET /api/products?limit=20&offset=0
GET /api/products?limit=20&offset=20
GET /api/products?limit=20&offset=40

This approach is straightforward and works well for many relatively stable datasets. It is also easy to combine with sorting and filtering parameters.

A major limitation appears when the offset becomes very large. Depending on the database and query, the server may need to scan or skip a large number of rows before returning the requested page.

Cursor-Based Pagination

Cursor pagination uses a value representing the position of the last item or another server-defined position. Instead of saying how many records to skip, the client tells the API where to continue.

GET /api/products?limit=20
GET /api/products?limit=20&cursor=eyJpZCI6MjB9

The cursor is usually returned by the previous response. It may contain an encoded identifier, timestamp, sort value, or another piece of state needed to locate the next group of records.

{
  "data": [
    {
      "id": 101,
      "name": "Keyboard"
    },
    {
      "id": 102,
      "name": "Mouse"
    }
  ],
  "pagination": {
    "nextCursor": "eyJpZCI6MTAyfQ=="
  }
}

The client does not normally need to interpret the cursor. It can send the value back to the API exactly as provided.

Keyset Pagination

Keyset pagination is closely related to cursor pagination. Instead of skipping a number of rows, the query uses the values of an ordered key to find records after a known position.

SELECT id, name
FROM products
WHERE id > 100
ORDER BY id ASC
LIMIT 20;

Here the database can use the indexed id column to find records after 100 instead of scanning all preceding records. This can make pagination more efficient for large datasets when the ordering key is suitable for keyset queries.

A cursor-based API may hide this implementation detail behind an opaque cursor. Keyset pagination describes the database-side technique, while cursor pagination describes how the continuation position is exposed to the client.

Offset vs Cursor Pagination

CharacteristicOffsetCursor
Simple to implementUsuallyModerate
Easy numbered pagesYesUsually not
Large datasetsCan become expensiveOften better suited
Stable under insertsCan shift resultsUsually more stable
Jump directly to pageEasyUsually difficult
Client implementationSimpleRequires cursor handling
Suitable for feedsLess suitableOften suitable

Neither approach is universally correct. Offset pagination is often convenient for administrative interfaces and smaller collections, while cursor or keyset pagination can be more appropriate for large datasets and continuously changing feeds.

The Importance of Stable Sorting

Pagination should normally be combined with a deterministic sort order. Without a stable ordering rule, the same item may appear on multiple pages or disappear between requests.

GET /api/products?limit=20&sort=createdAt:desc

A common solution is to sort by a primary field and use a unique identifier as a tie-breaker. For example, records can be ordered by createdAt and then by id.

SELECT id, name, created_at
FROM products
ORDER BY created_at DESC, id DESC
LIMIT 20;

The unique secondary field makes the ordering deterministic when multiple records have the same timestamp.

⚠️ Avoid relying on database row order without an explicit ORDER BY clause. An unspecified order should not be treated as stable pagination order.

Pagination and Changing Data

Pagination becomes more complicated when records are inserted, deleted, or updated between requests. Consider an offset-based request for page 1 followed by page 2. If several new records are inserted at the beginning between those requests, the offset for page 2 may now point to a different position.

This can result in duplicate records or missing records while the client moves through the collection. The problem is especially noticeable in feeds ordered by newest records first.

Cursor and keyset approaches can reduce this problem because the next request is based on a known position rather than an absolute number of skipped records. They do not automatically solve every consistency problem, but they generally provide a better foundation for moving through changing collections.

Designing a Paginated Response

A paginated response should give clients enough information to retrieve the next or previous portion of the collection without exposing unnecessary implementation details.

{
  "data": [
    {
      "id": 101,
      "name": "Keyboard"
    },
    {
      "id": 102,
      "name": "Mouse"
    }
  ],
  "pagination": {
    "limit": 20,
    "nextCursor": "eyJpZCI6MTAyfQ==",
    "hasNextPage": true
  }
}

The exact response structure is a design choice. Some APIs expose links instead of a dedicated pagination object, while others include totals, page numbers, cursors, or boolean indicators.

Pagination Metadata

Useful metadata depends on the pagination strategy and client requirements. Common values include the requested page size, current page, total pages, total item count, next cursor, previous cursor, and indicators showing whether more results exist.

MetadataUseful for
limitKnowing the requested or applied page size
offsetUnderstanding the current collection position
pageNumbered page interfaces
totalDisplaying the collection size
totalPagesCalculating numbered navigation
hasNextPageDetermining whether more results exist
nextCursorFetching the next cursor-based page
previousCursorMoving backward through cursor-based results

Not every API needs all of these values. Returning an exact total count, for example, may require additional database work and may not be useful for an infinite-scroll interface.

Should APIs Return a Total Count?

A total count can be useful when a client displays numbered pages or text such as '1–20 of 4,000'. However, calculating the total may be expensive for very large datasets, especially when complex filters are involved.

Cursor-based APIs often expose hasNextPage or a next cursor instead of an exact total. This avoids requiring the server to calculate the size of the entire filtered collection on every request.

💡 Return metadata that the client actually needs. Avoid calculating expensive totals simply because they are traditional pagination fields.

Limit Maximum Page Size

Clients should not normally be allowed to request an unlimited number of records. An API can define a default page size and a maximum permitted value.

GET /api/products?limit=50

For example, an API might use 20 as the default and cap requests at 100. The exact values depend on response size, database performance, and the expected clients.

⚠️ Do not assume that a client-controlled limit is safe. Validate pagination parameters and enforce server-side maximums to prevent unexpectedly expensive requests.

Validate Pagination Parameters

Pagination parameters are still user input and should be validated like other query parameters. Negative offsets, zero or negative page sizes, invalid cursors, and excessively large limits should be handled explicitly.

InputPossible handling
limit=20Accept
limit=0Reject or apply a documented minimum
limit=100000Reject or cap at the maximum
offset=-1Reject
page=abcReturn a validation error
Invalid cursorReturn a documented cursor error

Pagination with Filtering

Pagination is often combined with filtering. For example, a products API might allow clients to request only products belonging to a category.

GET /api/products?category=keyboards&limit=20&offset=40

The filtering conditions must be applied consistently before pagination. Otherwise, the API may return incorrect page boundaries or unexpected result counts.

For cursor-based pagination, the cursor must also represent a position within the filtered and sorted result set. Changing the filter while reusing an old cursor can produce invalid or unexpected results.

Pagination with Sorting

Sorting and pagination are closely related because the pagination position depends on the order of records. Clients should be able to understand which sort order is being used and should not arbitrarily change it while continuing from an existing cursor.

GET /api/orders?sort=createdAt:desc&limit=20
GET /api/orders?sort=createdAt:desc&limit=20&cursor=abc123

A cursor should generally be treated as belonging to the query context that created it. If filtering or sorting changes, the previous cursor may no longer represent a meaningful continuation position.

Pagination with Search

Search endpoints can also be paginated. A request might combine a search query, filters, sorting, and pagination parameters.

GET /api/products?q=keyboard&limit=20&offset=0

Search results can change as the underlying index changes. For applications that need stable traversal through large result sets, cursor-style approaches can be preferable to repeatedly increasing an offset.

Database Performance and Pagination

Pagination is not only an API design concern. The database query behind the endpoint determines much of its actual performance.

An offset query may become increasingly expensive as the offset grows because the database may need to process many rows before producing the requested page. An indexed keyset condition can allow the database to seek directly to a later position.

SELECT id, name
FROM products
WHERE id > 500000
ORDER BY id ASC
LIMIT 50;

Indexes should be designed around the filters and ordering used by the endpoint. The exact indexing strategy depends on the database engine, query shape, data distribution, and workload.

Cursor Design

Cursors should generally be treated as opaque values by clients. The client should not need to know whether the cursor contains an ID, timestamp, compound key, or another internal representation.

{
  "pagination": {
    "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA5LTIxVDEwOjAwOjAwWiIsImlkIjoxMDJ9"
  }
}

An opaque cursor gives the server freedom to change its internal representation later. It can also prevent clients from becoming dependent on database-specific details.

⚠️ Do not require clients to decode or construct cursors. A cursor should normally be generated and interpreted by the API itself.

Cursor Security and Validation

A cursor should be validated when it is received. Depending on its design, the server may need to detect malformed, expired, tampered, or incompatible cursors.

If a cursor contains sensitive or implementation-specific information, the API can encode or protect that information rather than exposing it directly. The cursor should not be treated as trusted input simply because the API generated it previously.

Previous and Next Pages

Forward pagination is usually simpler than backward pagination. A client can request the next set of records using a next cursor, while moving backward may require a previous cursor or a reversed query.

{
  "pagination": {
    "previousCursor": "abc123",
    "nextCursor": "xyz789"
  }
}

If an API supports backward navigation, the cursor semantics and ordering rules should be documented carefully. Clients should not have to infer whether a cursor represents the first, last, or currently visible record.

Pagination Links

An API can expose links for navigating between pages instead of requiring clients to construct URLs themselves. This can make the response self-describing and reduce client-side knowledge about pagination parameters.

{
  "data": [
    {
      "id": 101,
      "name": "Keyboard"
    }
  ],
  "links": {
    "self": "/api/products?limit=20&offset=40",
    "next": "/api/products?limit=20&offset=60",
    "previous": "/api/products?limit=20&offset=20"
  }
}

Link structures are particularly useful when clients should follow server-generated pagination URLs rather than constructing them from assumptions about the API.

Pagination and HTTP Caching

Each paginated request can have a different cache key because its query parameters or cursor are different. Caching can reduce repeated database work when clients request the same pages frequently.

However, frequently changing collections can make cached pages stale. Cache behavior should therefore be considered together with the freshness requirements of the endpoint.

Pagination for Infinite Scrolling

Infinite scrolling interfaces typically load another batch when the user approaches the end of the currently displayed results. Cursor-based pagination fits this interaction naturally because the client can request the next batch using the cursor returned by the previous response.

Offset pagination can also support infinite scrolling, but changing data can cause duplicates or gaps when offsets move relative to inserted or deleted records.

Pagination for Numbered Interfaces

Traditional tables and search interfaces often display numbered pages because users may want to move directly to a particular page. Offset or page-based pagination is convenient for this use case because the client can calculate the requested position directly.

If the dataset is very large, however, jumping to a distant page can become expensive with offset queries. In that situation, the interface may need a different navigation model or an API designed around cursors.

Pagination Errors

Invalid pagination parameters should produce predictable errors. Clients should be able to distinguish an invalid request from an empty page.

{
  "error": {
    "code": "INVALID_PAGE_SIZE",
    "message": "pageSize must be between 1 and 100"
  }
}

An empty page is normally not the same as an invalid request. For example, a valid request may simply reach the end of the collection and return an empty data array.

Pagination in OpenAPI

OpenAPI can document pagination parameters so that developers can understand how to request different portions of a collection. Query parameters such as limit, offset, page, and cursor can be described directly in the endpoint specification.

parameters:
  - name: limit
    in: query
    schema:
      type: integer
      minimum: 1
      maximum: 100
      default: 20

  - name: offset
    in: query
    schema:
      type: integer
      minimum: 0
      default: 0

The specification should also describe pagination metadata in response schemas and provide examples where the behavior is not obvious. This makes the pagination contract easier to consume and test.

Common Pagination Mistakes

One common mistake is returning an unlimited collection and expecting clients to control how much data they can handle. The server should enforce reasonable limits even when the client provides a large value.

Another mistake is paginating without a deterministic sort order. If the order changes between requests, clients can receive duplicates or miss records.

Using large offsets on very large tables can also lead to poor database performance. In those situations, keyset or cursor-based pagination may provide a more efficient approach.

A further mistake is exposing cursor internals to clients. Clients should generally treat cursors as opaque values and send them back without attempting to interpret or modify them.

Choosing a Pagination Strategy

Use casePotential approach
Small, stable collectionOffset or page-based pagination
Administrative tablePage-based or offset pagination
Numbered search resultsPage-based or offset pagination
Large datasetCursor or keyset pagination
Frequently changing feedCursor or keyset pagination
Infinite scrollingCursor pagination
Direct page navigationPage-based or offset pagination

These are starting points rather than strict rules. The final choice should account for database behavior, client requirements, consistency expectations, filtering, sorting, and the expected size and growth rate of the dataset.

A Practical Pagination Design Checklist

  • Choose a pagination strategy appropriate for the dataset and user interface.
  • Define a default page size.
  • Set a server-side maximum page size.
  • Validate all pagination parameters.
  • Use deterministic sorting for collection results.
  • Document the meaning of every pagination parameter.
  • Decide whether exact total counts are actually necessary.
  • Return clear metadata for continuing pagination.
  • Treat cursors as opaque values.
  • Consider database indexes for the chosen filtering and ordering.
  • Test pagination when records are inserted and deleted.
  • Document behavior at the beginning and end of a collection.

Frequently Asked Questions

What is pagination in a REST API?

Pagination divides a large collection into smaller responses so that clients do not have to retrieve every record at once. The client requests a limited portion of the collection and can then retrieve additional portions.

What is the difference between offset and cursor pagination?

Offset pagination identifies a position by the number of records to skip, while cursor pagination uses a continuation value returned by the API. Cursor-based approaches can be more suitable for large or frequently changing datasets.

What is the difference between page and offset pagination?

Page-based pagination uses a page number and page size, while offset pagination directly specifies how many records to skip. A server can internally translate page numbers into offsets.

Should every REST API use pagination?

Collection endpoints that can grow significantly usually benefit from pagination. Very small collections may not need it, but APIs should consider how the dataset could grow over time rather than relying only on its current size.

What is a cursor in API pagination?

A cursor is a value representing a position in a result set. The API typically returns a cursor with one response and the client sends it back to request the next or previous portion of the collection.

Should an API return the total number of records?

Not necessarily. Exact totals are useful for numbered pagination interfaces but can be expensive to calculate for large or complex datasets. Cursor-based APIs often use indicators such as hasNextPage instead.

Why is stable sorting important for pagination?

Pagination depends on a predictable ordering of records. Without deterministic sorting, records can move between pages, causing duplicates or missing items when clients make multiple requests.

Helpful API Pagination Tools

Pagination is easier to test when requests, query parameters, response structures, and API contracts can be inspected separately. REST API mock generators can simulate paginated collections and different page responses. HTTP request builders are useful for testing limit, offset, page, and cursor parameters, while query parameter builders can help construct complex filtered and paginated URLs. OpenAPI viewers make pagination parameters and response schemas easier to inspect, and JSON formatters can make paginated response metadata and nested result structures easier to read.

Conclusion

Pagination is an essential part of designing REST APIs that work with growing collections. It limits response sizes, reduces unnecessary database and network work, and allows clients to retrieve data incrementally.

Page-based and offset pagination are straightforward and work well for many traditional interfaces. Cursor and keyset pagination can be more appropriate for large datasets, infinite scrolling, and collections that change frequently. Regardless of the strategy, a good pagination design should use deterministic sorting, validate client input, enforce reasonable limits, document its metadata, and account for the behavior of the underlying database.

Found an issue?

Found an error, outdated information, or something missing from this article? Let me know through the Contact page.

Your feedback helps improve our articles and keep them accurate and useful.