How to Version an API Without Breaking Existing Clients
When you change an API, software that already calls it may keep sending the old requests and expecting the old responses. An…
When an API or database returns a large list, pagination lets a client fetch it in smaller pages. With offset pagination, the client requests a page by specifying how many records to skip. Cursor pagination sends a marker for the last record it received, so the next request can continue from that position.
Use cursor pagination for long lists that change as people or services read them, including activity feeds and logs, as well as event streams. It avoids the growing cost of scanning deep offset pages and reduces skipped or repeated records when new rows appear. Use offset pagination when users need numbered pages or direct access to a specific page, especially for small result sets that change little.
Offset pagination combines a page size with a count of records to skip. For example, a request for a page after skipping records on earlier pages asks the database for the next page. Because the client supplies the skip count, it can also jump directly to a later page.
Offset pagination has two costs. First, the database generally walks past skipped rows before returning the requested page. Deep pages get slower as the offset grows, even when the database uses an index. Second, inserts or deletes between requests shift row positions. A new record before the next offset can make the client receive a record it already saw. A deleted record can make it skip a record.
Offset pagination still fits small lists and interfaces where page numbers matter. Developers can implement it straightforwardly when the data changes infrequently. For a large list, test the deepest pages users actually request rather than timing only the first page. PostgreSQL pagination comparisons describe how the cost of skipping rows grows with the offset.
The client sends a cursor marking where the previous page ended with the next request, and the server returns records that follow that position in the chosen order. Many APIs expose the marker as an opaque string, which clients pass back without interpreting.
A common database method is keyset pagination. The query compares each row’s sort key with the last row on the previous page. For example, a feed might sort records by creation time, newest first. If two records can have the same timestamp, the query also sorts by a unique ID. The cursor carries both values, and the next query selects records that come earlier in that combined order. The unique ID breaks ties, so records with the same timestamp keep a consistent order.
For efficient comparisons, the database needs an index that matches the sort order and cursor fields. Without that index, keyset pagination loses much of its performance advantage. PostgreSQL keyset examples show how a query can seek from the last key instead of scanning and discarding earlier rows.
The server can encode the cursor’s values as a string to keep their representation out of the API contract. Encoding does not itself make a cursor secret or secure. The client should treat the value as an instruction to continue, not as a page number or an ID it can safely modify. In GraphQL’s Cursor Connections specification, for example, each edge includes a cursor, and clients request forward pages with first and after or backward pages with last and before.
A cursor marks a position in the sort order, so inserts before it do not shift the start of the next page. Sequential reads are more stable than offset reads as records are added or removed. An indexed query can also seek directly to the next range instead of doing work proportional to a large skip count.
Cursor pagination does not create a frozen snapshot of the list. If a record’s sort key changes while a client pages through results, that record can move across the cursor boundary. A new record inserted after the current position can also appear in a later page. If a data export or synchronization job needs a fixed view, the service must provide a separate snapshot or consistency mechanism; a cursor alone does not guarantee one.
Cursor pagination also gives up direct page-number access. A client can continue from a cursor it has saved, but it cannot generally jump directly to an arbitrary page without first traversing earlier results or using some separate lookup mechanism. The server must also apply cursor parameters correctly. A reported Stripe MCP server issue illustrates the failure mode: when a list endpoint ignores its pagination parameters, repeated requests return the first page again.
Clients should use a cursor to move through a large result set one position at a time, especially as new records arrive. This suits infinite scroll, feeds, logs, and event streams. Before shipping, check that the query uses a stable, unique sort order and an index that matches the sort fields. Also check that each response includes a cursor and that the client sends it with the next request.
Use an offset when users need to jump among numbered pages. It also fits when the result set is small enough that skipping rows stays inexpensive, or when page positions matter more than consistent traversal during concurrent changes. If both needs exist, an API can offer offset pagination for page-number navigation and cursor pagination for sequential browsing, while making the different consistency and performance behavior clear.
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