larsggu.me › Reference › cursor pagination
cursor pagination
GET /collection?limit=n[&after=<cursor>] -> {data, next_cursor}
Paging through a collection with an opaque position marker rather than a numeric offset, so that concurrent writes do not cause rows to be skipped or repeated.
Description
Offset paging asks for rows one hundred to one hundred and fifty of an ordering that is changing underneath the reader. If a row before the window is inserted between two requests, everything shifts and one row is read twice; if a row is deleted, one row is never read at all. On a busy collection this is not a rare condition but the ordinary case.
A cursor names a position rather than a distance. The service returns an opaque token that encodes where the previous page ended in the sort order it used, and the next request resumes from exactly there. Insertions and deletions elsewhere in the collection no longer move the reader's place.
The token is opaque on purpose. Callers that decode it and construct their own end up coupled to a sort key the service intends to change, and they break silently when it does. The only supported operations are to pass it back unmodified or to discard it and start again.
Termination is by the absence of a next cursor rather than by a short page. A page smaller than the requested limit is permitted at any point, so a caller that stops on a short page truncates its own read. Where an integration needs to keep up with a collection rather than read it once, the same cursor mechanism is ordinarily exposed as a resumable position the caller stores between runs.
Fields
| Field | Form | Meaning |
|---|---|---|
| limit | integer | Maximum rows in the page. The service may return fewer at any time. |
| after | opaque cursor | Resume position. Omitted on the first request. |
| data | array | The page of rows, in the service's stated order. |
| next_cursor | opaque cursor or null | Present while more rows remain. Null or absent ends the read. |
| order | stated sort | The ordering the cursor is defined against. Changing it invalidates held cursors. |
Example
Reading a collection to the end
GET /v1/invoices?limit=2 HTTP/1.1
HTTP/1.1 200 OK
{"data":[{"id":"in_3390"},{"id":"in_3391"}],
"next_cursor":"Y3Vyc29yOjMzOTE"}
GET /v1/invoices?limit=2&after=Y3Vyc29yOjMzOTE HTTP/1.1
HTTP/1.1 200 OK
{"data":[{"id":"in_3392"}],
"next_cursor":null}The read ends because next_cursor is null, not because the final page was short.
Failure modes
- Stopping on a short page, which silently truncates the read.
- Decoding the cursor and rebuilding it, which couples the caller to an internal sort key.
- Holding a cursor across a change of ordering, which resumes at a position that no longer means what it meant.
- Reading with no limit at all and taking whatever default the service applies, which makes the run time of the job unpredictable.
Related entries
Topic: Transport. Last modified 2026-09-06.