Pagination (Cursor vs Offset)

Fetching a long list in chunks: offset counts how far in you are, cursor hands you a bookmark for where the last chunk stopped.

only gives me 100 at a timehow do I get the next batchpaginatonnext page tokenlimit and offsetload more button datainfinite scroll fetchingwhy do rows repeat between pages

See it

Live demo coming soon

What it is

APIs refuse to hand you a million rows at once, so they slice results into pages and tell you how to ask for the next slice. Two flavors do the telling. Offset pagination counts positions: 'skip 200, give me 20', written as limit=20 and offset=200, or as page=11. Cursor (keyset) pagination hands back an opaque bookmark for the last item you saw, and you send it right back as something like after=eyJpZCI6MTIzfQ.

Offset is easy to reason about and lets you jump to page 47, which is why it survives in admin tables. Cursor pagination is the usual pick for large or fast-changing datasets, Stripe's list endpoints being the textbook example, because it stays fast at depth and does not shuffle when rows are inserted mid-scroll. Plenty of big APIs still offer page numbers or offsets, sometimes alongside cursors in the same product, so read the docs for the specific endpoint instead of assuming. Reach for cursors for feeds, infinite scroll, and any sync loop; reach for offset when a human needs numbered pages over a mostly static list.

The classic bug: with offset, someone adds a record while you are walking pages, everything shifts down one, and you see item 200 twice while item 220 never appears. Also, an SQL offset of 100,000 makes the database count through 100,000 rows before returning anything, so deep pages get slower and slower. Never build a cursor yourself out of the last ID unless you sort by something unique and stable.

Ask AI for it

Implement cursor (keyset) pagination on this list endpoint. Accept a 'limit' param (default 20, max 100) and an opaque 'cursor' query param. Sort by (created_at, id) descending, fetch limit + 1 rows to detect whether more exist, and return { data, nextCursor, hasMore } where nextCursor is the base64 of the last row's (created_at, id). Return nextCursor as null on the final page. On the client, write a fetch loop that follows nextCursor until it is null, and show me the offset version side by side so I can compare.

You might have meant

rate limitendpointbatch bulk endpointsync cursor watermarkpolling