Offset pagination
GET /orders?page=3&per_page=20
-- SQL: SELECT * FROM orders LIMIT 20 OFFSET 60
Simple. Works with any SQL database. Problems: unstable results when items are inserted/deleted during pagination; expensive on large offsets (the DB scans and discards all preceding rows).
Cursor pagination
GET /orders?cursor=eyJpZCI6MTIzfQ&limit=20
-- Decoded cursor: {"id": 123}
-- SQL: SELECT * FROM orders WHERE id > 123 ORDER BY id LIMIT 20
Stable results regardless of concurrent inserts/deletes. O(log n) with an index — efficient at any offset depth. Cannot jump to page N directly. Best choice for infinite scroll, feed-style UIs, and large datasets.
Keyset pagination
Generalisation of cursor pagination for multi-column sorts:
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC LIMIT 20
Handles non-unique sort columns by including a tiebreaker (usually the primary key).
Response envelope
{ "data": [...], "pagination": { "next_cursor": "eyJpZCI6MTQzfQ", "has_more": true } }