API Pagination Strategies: Offset vs Cursor-Based

API pagination seems simple until you’re paginating through data that’s actively changing — the two dominant strategies handle that differently, with real correctness implications.

# Offset-based: simple, but breaks under concurrent inserts
GET /api/products?offset=20&limit=10
# If a new product is inserted at position 5 while paginating,
# every subsequent page shifts by one - a client can see a duplicate
# OR silently skip an item entirely

# Cursor-based: stable regardless of concurrent changes
GET /api/products?after=eyJpZCI6MTIzfQ&limit=10
# The cursor encodes a specific position (e.g. the last seen ID),
# not a numeric offset - immune to items being inserted/deleted elsewhere

Why Cursor-Based Wins for Active Data

Offset pagination computes “skip the first N rows” fresh on every request — if the underlying data shifts between page 1 and page 2 requests, the offset no longer points at a consistent logical position. Cursor-based pagination instead anchors to a specific row (typically via its ID or a composite sort key), remaining stable regardless of what else changes in the table.

The Trade-off

Cursor-based pagination can’t easily jump to an arbitrary page number (“show me page 47”) the way offset pagination can — you can only move forward or backward from a known cursor. For infinite-scroll style interfaces this limitation rarely matters; for a paginated admin table with page-number navigation, it’s a genuine trade-off worth considering.