Cursor-based pagination for stable lists
Use opaque cursors instead of numeric offsets for more stable pagination over changing data like feeds, logs, or events.
Cursor-based pagination uses an opaque cursor (often a token derived from the last item) rather than numeric offsets. This makes pagination more stable under concurrent writes and more efficient for large tables.
Example: fetching the first page.
GET /events?limit=20Accept: application/jsonHTTP/1.1 200 OKContent-Type: application/json
{ "data": [ { "id": "evt_101", "created_at": "2026-01-13T15:00:00Z" }, { "id": "evt_100", "created_at": "2026-01-13T14:59:59Z" } ], "paging": { "next_cursor": "eyJpZCI6ICJldnRfMTAwIiwgImNyZWF0ZWRfYXQiOiAiMjAyNi0wMS0xM1QxNDo1OTo1OVoiIH0=", "has_more": true }}Next page:
GET /events?limit=20&cursor=eyJpZCI6ICJldnRfMTAwIiwgImNyZWF0ZWRfYXQiOiAiMjAyNi0wMS0xM1QxNDo1OTo1OVoiIH0=Accept: application/jsonTrade-offs and notes 
Pros
-
Stable ordering; avoids missing or duplicating items when new rows are inserted.
-
Efficient for large datasets when implemented with “seek” queries (
WHERE created_at < ?).
Cons
-
Slightly more complex for clients than
page=3. -
Harder to jump to arbitrary pages; it’s more “scroll” than “go to page 7”.
DX tips
-
Keep the cursor opaque; don’t require clients to parse it.
-
Always include a
has_moreflag so clients know when to stop. -
Stick to a deterministic sort order (for example
created_at DESC, id DESC).