Normalizing API Pagination for AI Agents
When an AI agent reads records from an API, the API tells the client how to request the next page. One API…
When a REST API returns a set of records, pagination splits the records across multiple responses. Without pagination, a request for many records can return a large amount of data and make the server do more work at once. Add pagination when you design a collection endpoint, before clients rely on receiving every record in one response.
Choose a pagination method based on how clients move through the results. Page numbers support direct jumps. An offset tells the server how many records to skip, which also lets clients jump to a particular point. A cursor is a marker that tells the server where to continue; cursors and continuation links suit clients that browse one page at a time, especially far into the results. Set a page-size limit for either method. Tell clients how to find the next page, and return results in a consistent order.
With offset pagination, the server skips a set number of records and returns the next set. A page-number interface can turn the requested page and page size into the number of records to skip. This approach is easy to understand and supports direct jumps, but the database still has to work through the skipped rows. PostgreSQL warns that large offsets can be inefficient. It also explains that LIMIT and OFFSET need an ORDER BY clause that sets a consistent order to return consistent subsets.
Cursor pagination uses a marker to show where the server should continue. The server returns records after that position, often using the last item’s values for the fields it sorts by. A continuation link or token gives the client what it needs to request the next page. Cursor pagination works well when clients browse one page at a time, but it does not necessarily let them jump to any numbered page.
| Approach | Use it when | Trade-off |
|---|---|---|
| Page number or offset | Clients need to jump to numbered pages and offsets will remain manageable. | Large offsets require the database to compute rows it will not return. |
| Cursor or continuation | Clients move through a large collection a page at a time. | Clients follow the available continuation path rather than jump to any page. |
Neither approach is a universal default. JSON:API leaves the pagination strategy to the API, while Microsoft Graph documents both endpoint-specific paging behavior and continuation links. Pick the method that fits the client’s navigation needs and the database query you can support.
Give each collection endpoint a documented page-size parameter, default, and maximum. Reject invalid values or define how the server handles them. A maximum limits the size of each response; it does not guarantee that every request will return that many items.
Tell clients clearly when the collection has ended. The server might return a next link only when another page exists, or include a token that is empty at the end. Clients should follow that signal instead of assuming a short page means there are no more results. For example, Google’s AIP-158 pagination guidance allows a server to return fewer items than requested, including zero, even when more results remain. Microsoft Graph uses a different contract: clients follow @odata.nextLink while it appears, and stop when it is absent.
Page sizes vary by product and endpoint. For GET /users, Microsoft Graph documents a default of 100 results. GitHub’s pagination documentation shows an issues response with 30 items. Stripe’s Report Runs endpoint documents a limit from 1 to 100, with a default of 10. Treat those as examples of endpoint-specific choices, not universal recommendations. Set a default and maximum that suit your response size and backend, then measure real requests.
Totals also need a clear contract. If clients need a total for page navigation, document whether it is exact, estimated, or unavailable. Google’s guidance permits an estimated total when the API identifies it as an estimate. Do not promise an exact count without checking what that query costs for your data and database.
Return records in a predictable order so they do not move unexpectedly between pages. A sort field with duplicate values leaves the order unclear for records with the same value. Add a unique tie-breaker, such as an ID, to decide which of those records comes first.
For example, an API that sorts orders by creation time can break ties with the order ID. The API should document the sort fields and direction, and apply the same ordering when it creates each continuation. PostgreSQL’s LIMIT and OFFSET documentation explains why an order that does not uniquely constrain rows can produce inconsistent subsets.
A stable sort does not freeze the collection while a client reads it. New, deleted, or updated records can affect later pages according to the API’s consistency behavior. Document what clients should expect when data changes during pagination; do not imply that a consistent order guarantees a snapshot of the whole collection.
When the server returns a continuation token or full next URL, clients should reuse it as provided. The token is opaque, meaning clients should treat its contents as unknown. They should not decode it, assemble a replacement URL, or assume a particular format. Microsoft Graph likewise tells clients to use the complete @odata.nextLink as returned.
The token should identify where the server should continue and should not grant access to records. The server must still authorize every request. Bind the token to the relevant query context, such as the filters and sort order, and document whether clients can change the page size between requests. Token lifetime and expiration behavior also belong in the API contract.
A response can put navigation URLs in its body or use HTTP’s Link header. The JSON:API specification defines common link names such as next and prev while leaving the pagination strategy open. RFC 8288 defines how HTTP link relationships are represented in headers; it does not require paginated APIs to use that header.
This example shows one response shape; parameter names and URL format are API-specific:
{
"data": [
{
"id": "ord_123",
"created_at": "2025-04-10T12:00:00Z"
}
],
"links": {
"self": "/v1/orders?limit=50&sort=-created_at,-id",
"next": "/v1/orders?limit=50&sort=-created_at,-id&cursor=opaque-value"
}
}
The client follows next until the API omits it or returns the documented end signal. The client should not assume the example’s field names or sort syntax apply to another API.
Pagination bounds the amount of data in one response, but it does not make every query fast. The database still has to filter, sort, and retrieve the requested records. Deep offsets add work for rows the server skips; a cursor avoids that skip only when the query and sort support an efficient lookup.
Check the query plan, the steps the database expects to take, for the endpoint’s common filters and sort order. A database index can help when its columns match the fields those requests filter and sort by. PostgreSQL’s multicolumn index guidance notes that a B-tree index works best when conditions use its first columns. It also cautions that adding multicolumn indexes has costs. Build indexes for queries you observe, keeping the set focused rather than covering every possible combination of filters and sorts.
Use offset pagination when clients need to jump directly to numbered pages and the expected offsets remain practical. Use cursor or continuation pagination when clients need to browse many pages in order and the backend can support the matching sort and lookup efficiently. Test both the first page and pages far into the results with production-like data. A cursor alone does not guarantee a faster query.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com