A list endpoint may work well with a few dozen records and become slow or confusing when a tenant has thousands. Pagination controls how much data the API returns and how users move through it. Offset and cursor approaches solve different problems, and neither is automatically superior. The choice should follow the product's navigation needs, expected data growth, and the order in which items are shown.
Start with stable ordering
A paginated list needs a deterministic sort. If records share the same creation time, add a unique tie-breaker such as an item ID. Without it, a record can appear on two pages or disappear between requests even when no data changes. Define default ordering in the API rather than letting each client decide. Filters and tenant scope must be applied before pagination, and indexes should support the combination of tenant, filter, and sort used most often. A fast query that returns an unstable sequence is still a poor user experience.
Where offset works
Offset pagination uses a page number or skip count and limit. It is easy for users to understand and supports jumping directly to a known page. It can be suitable for modest administrative lists where record counts are not enormous and data changes slowly. Large offsets may require the database to walk through many earlier records, and inserts or deletes can shift page boundaries during navigation. If the product promises a total page count, computing that count may also be expensive for complex filters. Measure actual list sizes before replacing a simple offset design.
Where cursors help
Cursor pagination uses the last seen sort key to request the next set of records. It can be efficient for long feeds and stable traversal when new items arrive. The cursor should include the fields needed to continue the exact ordering and should be validated rather than treated as an arbitrary database query. It may be opaque to clients and encoded safely. Cursor pagination is less convenient for jumping to page 50 or displaying a precise total page count. Use it where the user mainly moves forward or backward through a changing sequence.
Handle changes during browsing
A user may open a list, pause, and continue after new records arrive. Decide whether pages should reflect the latest state or a snapshot. Cursor navigation can reduce duplicates from new records at the top, but updates to sort keys can still move items. For workflows that require a fixed export or audit view, generate a snapshot or use a stable cutoff time. Avoid promising an exact count if the underlying data changes continuously. Communicate a practical behavior in the UI, such as loading more recent items separately from older results.
Return useful metadata
The API should return the items in final order, a `hasMore` flag, and the next cursor or page information needed by the client. If the UI shows totals, calculate them on the server and state whether they are exact or approximate. Do not make the browser infer `hasMore` from the number of items, since a full final page can otherwise create a false next action. Enforce a maximum page size to protect the service. Apply authorization and tenant filtering consistently to both items and counts.
Choose proportionately
A small SMB panel may be well served by offset pagination with good indexes. A public feed or large activity log may benefit from cursors. Test representative tenants and filters, observe slow queries, and change strategy when the measured cost justifies it. Pagination is part of the API contract, so a migration should preserve the user's ability to navigate and avoid silent ordering changes. The best design makes lists predictable for customers and affordable for the team to maintain. Review search and export separately from interactive lists: those workflows may need a different boundary or a generated snapshot. Document the ordering fields so clients do not accidentally create their own incompatible pagination logic. Test the final page, an empty list, and a page where records are inserted between requests.