Cursor pagination
Paging through a list with an opaque pointer to the last item seen, instead of a numeric offset.
Offset pagination asks for "rows 100 to 120", which shifts whenever an earlier row is inserted or deleted — the reader sees duplicates or misses entries. Cursor pagination asks for "the 20 after this item", which stays correct under concurrent writes and lets the database seek directly instead of counting past skipped rows.
Full guide:
Cursor or offset pagination: which one breaks and when
— That is offset pagination behaving as designed. An offset names a position in a result set that shifts whenever an earlier row is inserted or deleted, so concurrent writes cause duplicates and gaps. Cursor pagination asks for the rows after a specific item instead, which stays correct under concurrent writes and stays fast at depth.
Key facts
- Offset pagination can skip or repeat rows when the underlying list changes between pages; cursor pagination cannot.
- Deep offsets get slower as they grow because the database still walks the rows it discards, while a cursor seeks directly to its position.
- A cursor must be based on a stable, unique, ordered key — a non-unique timestamp will drop rows that share a value.
- Cursors are opaque: clients should store and return them, never construct or parse them.
Frequently asked questions
Can I jump to page 50 with cursor pagination?
No, and that is the trade. Cursors give you correct sequential traversal, not random access. If arbitrary page numbers are a requirement, offsets are the only option and you accept the drift.
Why is my cursor skipping rows?
Almost always a non-unique sort key. Ordering by a timestamp alone drops rows sharing the same value at a page boundary; add a unique tiebreaker such as the primary key.
Machine-readable copy of this page:
/glossary/cursor-pagination.md