Pagination

Offset vs cursor pagination, limits, and stable ordering.

On this page

Why pagination is mandatory

Unpaginated collection endpoints create performance problems, timeouts, and huge memory usage. Pagination protects both your API and your clients.

Offset pagination

Offset pagination uses limit and offset. It is easy but can be slow at large offsets and can produce inconsistent results when data changes.

GET /api/orders?limit=50&offset=0
GET /api/orders?limit=50&offset=50

Cursor pagination

Cursor pagination uses an opaque cursor pointing to a position in a stable ordering. It is faster and more consistent for large datasets.

GET /api/orders?limit=50&cursor=eyJpZCI6OTg3fQ==

Stable ordering is critical

Pagination must use a deterministic sort order (e.g., createdAt + id). Without stable ordering, clients will see duplicates or missing items across pages.

Response shape (recommended)

Return items plus paging metadata (nextCursor or total for offset pagination).

Example: cursor response

{
  "items": [ { "id": 1 }, { "id": 2 } ],
  "nextCursor": "eyJpZCI6Mn0="
}

Limits and protection

  • Enforce a max limit (e.g., 100).
  • Reject insane limits (400) or clamp them server-side.
  • Document default limit behavior.

Common mistakes

  • Returning all records “because it's easier”
  • Using offset pagination with frequently-changing datasets
  • No stable ordering, causing duplicates across pages

Checklist

  • All list endpoints are paginated.
  • Ordering is stable and documented.
  • Max limit exists and is enforced.