Pagination in API Design
API pagination through stable ordering, offsets, cursors, and page tokens.
Pagination is not just about cutting a long list into smaller pages. It is about defining a stable way to walk through a changing dataset without missing records, repeating records unexpectedly, or forcing the database to do wasteful work.
The first design choice is the sort order. Every pagination scheme depends on it. If the list is ordered by a non-unique field such as created_at alone, records with the same timestamp can move between pages or appear twice. Production APIs usually sort by a stable compound key, such as created_at plus id, so the order is deterministic.
Offset and page based pagination
Offset pagination uses parameters like offset=200&limit=50. Page based pagination is the same idea with different naming, such as page=5&size=50. These approaches are easy for clients and useful in admin tools where jumping to page 12 is genuinely valuable.
The main weakness is database cost. Large offsets often require the database to scan and discard many rows before it returns the requested slice. The other weakness is instability under concurrent writes. If new records are inserted at the front of the list between requests, items can shift and the client may miss or duplicate rows while paging.
Keyset and cursor pagination
Keyset pagination asks for rows after a known key, for example after_id=102 or after=(2026-08-01T10:00:00Z,102). Cursor pagination packages that state into an opaque token so the server can evolve its internal query without breaking clients.
This is usually the best choice for user-facing feeds, transaction histories, and other large mutable lists. The database can use an index to continue from the last seen row instead of counting past earlier rows. It is also more stable when new data arrives.
The tradeoff is flexibility. Clients cannot jump to an arbitrary page easily, and cursors need careful encoding and validation. A good cursor should be opaque, signed or otherwise protected against tampering, and tied to the filter set that produced it. Reusing a cursor with different filters should fail cleanly.
Time based pagination
Time based pagination uses a timestamp window, such as records before 2026-08-01T10:00:00Z. This works well for logs, events, and append-heavy histories where time is already the primary navigation model.
The caveat is clock quality. If timestamps are not monotonic or records arrive late, clients may skip or repeat items. As with keyset pagination, use a tiebreaker field when timestamps are not unique.
Hybrid approaches
Some APIs combine techniques. A feed might expose cursor pagination for normal browsing and offset pagination only for internal dashboards. Another common hybrid is a cursor token that internally stores the last timestamp and ID pair.
Response design and operational details
A solid paginated response includes the items, the page size, and enough metadata to continue safely, usually next_cursor and sometimes previous_cursor. Be cautious with total_count. Clients like it, but exact counts can be expensive on large filtered datasets. Some teams return an estimate or omit the count on hot endpoints.
The biggest implementation mistake is treating pagination as a presentation detail rather than a query contract. Filters, sort order, and pagination token semantics must work together. If the underlying query can change between requests, document what consistency clients should expect.
In practice, use offset or page based pagination when human navigation matters more than database efficiency. Use keyset or cursor pagination for large, frequently changing datasets. The right choice depends less on fashion and more on how the data moves while clients are reading it.