# Pagination Connections return a page of records and information about whether another page exists. Use `nodes` for the records or `edges` when you also need individual cursors. A cursor is an opaque position in a result set, not a resource ID. ## Read forward Admin API ```graphql query ProductPage($first: Int!, $after: String) { products(first: $first, after: $after) { nodes { id name } pageInfo { hasNextPage endCursor } } } ``` Variables ```json { "first": 25, "after": null } ``` For the next request, pass the returned `endCursor` as `after`. Continue only while `hasNextPage` is true. Use an explicit page size within the field's documented limit; 25 is a useful starting point. The standard connections support at most 100 items per page. ## Keep the result definition stable Do not change filters, sort, store, currency, locale, or customer context while continuing with a cursor. Restart from the first page when any of those values changes. Do not decode a cursor or generate a guessed next cursor. For backward navigation, use `last` with `before` and the current page's `startCursor` on fields that support that direction. Read `hasPreviousPage` to determine whether another previous page exists. Use one pagination direction per request. ## Account for changing data A long traversal is not a frozen snapshot of a live catalog. Records can change while you are paging. Deduplicate by resource ID when building a local mirror and run reconciliation after a long synchronization. Use a supported export job when its consistency and output behavior better fits the task. Request `totalCount` when your interface needs it. Do not use it as a substitute for `hasNextPage`, and do not stop because one filtered page has fewer records than you expected. See [Add search and filters](/guides/storefront/search-and-filters) for interface state and [Bulk operations and jobs](/guides/integrations/bulk-and-jobs) for larger data workflows.