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.
You need robust pagination over changing data like feeds, logs, or events.
Datasets are tiny or mostly static and offset/limit is sufficient.
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=20
Accept: application/json
HTTP/1.1 200 OK
Content-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/json
Trade-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).