Skip to content

Pagination

APIs often need to provide collections of data. However, collections can often be arbitrarily sized and also often grow over time, increasing lookup time as well as the size of the responses being sent over the wire. Therefore, it is important that collections be paginated.

This decision is not purely a UX decision, nor is it purely a technical one. UX requirements are a valid and important input, but must be weighed against dataset characteristics and performance rather than treated as the deciding factor in isolation. A great UX with offset pagination is of no use if the underlying dataset cannot support it reliably. Before choosing offset, teams must evaluate both user experience and technical limitations.

Use cursor-based pagination when:

  • The dataset is large, unbounded, or expected to grow significantly over time.
  • The underlying database is NoSQL or sharded. These databases are often designed around cursor-style access and may not support offset scanning at all or do so at significant performance cost.
  • The data changes frequently, as offset pagination may produce duplicates or skip items between page requests.
  • Sequential traversal (next/previous) is enough for the use case.

Use offset-based pagination when:

  • The dataset is small, bounded, and unlikely to grow significantly.
  • The underlying database is relational and the paginated query can be efficiently indexed.
  • The data is stable and unlikely to change between page requests.
  • Users must be able to jump to an arbitrary page, this is a validated user need and not just an assumed one.
  • UX requirements genuinely call for it, and the above technical factors do not contradict it.

Cursor-based pagination uses a pageToken which is an opaque pointer to a page that must never be inspected or constructed by clients. It encodes the page position (i.e., the unique identifier of the first or last page element), the pagination direction, and the applied query filters to safely recreate the collection.

When implementing cursor-based pagination:

  • Request messages for collections should define an integer pageSize query parameter, allowing users to specify the maximum number of results to return.
    • The pageSize field must not be required.
    • If the request does not specify pageSize, the API must choose an appropriate default.
    • The API may return fewer results than the number requested (including zero results), even if not at the end of the collection.
  • Request schemas for collections must define a string pageToken query parameter, allowing users to advance to the next page in the collection.
    • The pageToken field must not be required.
    • If the user changes the pageSize in a request for subsequent pages, the service must honor the new page size.
    • The user is expected to keep all other arguments to the method the same; if any arguments are different, the API should return a 400 Bad Request error.
  • Response messages for collections must define a string nextPageToken field, providing the user with a page token that may be used to retrieve the next page.
    • The field containing pagination results must be an array containing a list of resources constituting a single page of results.
    • If the end of the collection has been reached, the nextPageToken field must be empty. This is the only way to communicate “end-of-collection” to users.
    • If the end of the collection has not been reached, the API must provide a nextPageToken.
  • Responses should avoid including a total result count, since calculating it is a costly operation usually not required by clients.

Example:

GET /v1/publishers/123/books?pageSize=50&pageToken=abc123xyz

responds with:

{
"results": [
{
"id": "456",
"title": "Les Misérables",
"author": "Victor Hugo"
}
// ... 49 more books
],
"nextPageToken": "def456uvw"
}

Page tokens provided by APIs must be opaque (but URL-safe) strings, and must not be user-parseable. This is because if users are able to deconstruct these, they will do so. Tokens must never be inspected or constructed by clients. Therefore, tokens must be encoded (encrypted) in a non-human-readable form.

Page tokens must be limited to providing an indication of where to continue the pagination process only. They must not provide any form of authorization to the underlying resources, and authorization must be performed on the request as with any other regardless of the presence of a page token.

Some APIs store page tokens in a database internally. In this situation, APIs should expire page tokens a reasonable time after they have been sent, in order not to needlessly store large amounts of data that is unlikely to be used. It is not necessary to document this behavior.

When implementing offset-based pagination:

  • Request schemas for collections must define an integer pageNumber query parameter, allowing users to specify which page of results to return.
    • The pageNumber field must not be required and must default to 1.
  • Request schemas for collections must define an integer pageSize query parameter, allowing users to specify the maximum number of results to return.
    • The pageSize field must not be required.
    • If the request does not specify pageSize, the API must choose an appropriate default.
  • Response messages may include a total field indicating the total number of results available, though this should be avoided if the calculation is expensive.
  • The API may return fewer results than the number requested (including zero results), even if not at the end of the collection.

Example:

GET /v1/publishers/123/books?pageSize=50&pageNumber=2

responds with:

{
"results": [
{
"id": "456",
"title": "Les Misérables",
"author": "Victor Hugo"
}
// ... 49 more books
],
"total": 342
}

All collections must return a paginated response structure, regardless of size. For collections that will never meaningfully benefit from pagination, endpoints may satisfy this requirement by returning all results in a single response with an empty or absent nextPageToken, without implementing actual pagination logic. In other words, just wrap the results in the pagination envelope without actually implementing pagination.

However, if there is any reasonable chance the collection grows beyond a small size (typically a few hundred to low thousands of items), endpoints should implement true pagination from the start. Retrofitting pagination onto a collection that clients already consume as a single page is a breaking change.

paths:
/publishers/{publisherId}/books:
get:
summary: List books for a publisher
operationId: listBooks
parameters:
- name: publisherId
in: path
required: true
schema:
type: string
- name: pageSize
in: query
required: false
schema:
type: integer
default: 20
example: 50
description: >
The maximum number of results to return. The API will choose an
appropriate default if not specified. The API may return fewer
results than requested, even if not at the end of the collection.
- name: pageToken
in: query
required: false
schema:
type: string
example: abc123xyz
description: >
An opaque, URL-safe token used to advance to the next page of
results. This value is obtained from the `nextPageToken` field of a
previous response. Must not be inspected or constructed by clients.
responses:
'200':
description: A paginated list of books.
content:
application/json:
schema:
type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/Book'
description: |
The list of books for the current page.
nextPageToken:
type: string
description: >
An opaque token used to retrieve the next page of results.
If absent or empty, the end of the collection has been
reached. Pass this value as the `pageToken` query
parameter in a subsequent request to retrieve the next
page.
paths:
/publishers/{publisherId}/books:
get:
summary: List books for a publisher
operationId: listBooks
parameters:
- name: publisherId
in: path
required: true
schema:
type: string
- name: pageSize
in: query
required: false
schema:
type: integer
default: 20
example: 50
description: >
The maximum number of results to return. The API will choose an
appropriate default if not specified. The API may return fewer
results than requested, even if not at the end of the collection.
- name: pageNumber
in: query
required: false
schema:
type: integer
default: 1
example: 1
description: >
The page number to return, 1-indexed. Defaults to 1 if not
specified.
responses:
'200':
description: A paginated list of books.
content:
application/json:
schema:
type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/Book'
description: |
The list of books for the current page.
total:
type: integer
description: >
The total number of results available. This field is
optional and may not always be present, as calculating it
can be expensive.

Cursor-based pagination is generally better and more efficient than offset-based pagination. Cursor-based pagination maintains consistent performance regardless of dataset size, while offset-based pagination degrades as offsets increase. Many NoSQL databases are optimized for cursor-based access patterns. Cursor-based pagination provides consistent results even when data changes between requests, preventing items from being skipped or duplicated. It is also better suited for collections that are frequently updated. These advantages make cursor-based pagination the preferred approach for most use cases.

  • 2026-02-23: Change guidance to allow both offset and cursor. Remove the token offset option. Add guidance on when to choose each method.
  • 2026-01-30: Enforce camelCase, not snake_case for query parameters
  • 2025-12-15: Added guidance on token-based offset pagination for new APIs, small collection handling, and clarified that new APIs must use cursor-based or token-based offset pagination only.
  • 2025-12-10: Initial creation, adapted from Google AIP-158 and aep.dev AEP-158.