The journal
Is keyset pagination the same as cursor pagination?
A practical comparison of keyset vs cursor pagination, how they relate, and how to implement a scalable next/previous flow.

What each term means
Keyset (seek) pagination
Keyset pagination moves the query window using a predicate over the ordered columns, e.g. “give me rows with (created_at, id) less than the last seen values.” Because the database can seek into an ordered index and read only the next N rows, performance stays stable as users go deeper (GitLab pagination guidelines).
Typical pattern for the second page (ordering by a composite key with a stable tie-breaker):
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT :limitGitLab’s library generates correct predicates for multi-column and nullable sorts and requires a deterministic order; it also exposes helpers to get next/previous/first/last cursors (Keyset pagination | GitLab Docs).

Cursor pagination
Cursor pagination means the client sends an opaque position (a “cursor”) to continue a listing, typically via a query parameter or Link headers. With GitLab’s keyset library, the cursor literally encodes the last row’s ordered values; the server decodes that to build the keyset predicate and fetch the next window (Keyset pagination | GitLab Docs).
GitLab uses keyset pagination in both REST and GraphQL APIs for scalable listing and recommends avoiding page numbers and total counts in favor of next/previous flows (Pagination guidelines).
How they relate
- Different layers: keyset is the database access pattern; cursor is the API contract for passing position.
- Often combined: a cursor token wraps keyset state (for example, encoded order-by values) and the server seeks from that state to return the next page (Keyset pagination | GitLab Docs).
- Not interchangeable: you can implement a cursor over offset/page numbers, but it won’t fix deep-page performance. A keyset-backed cursor does.
When to choose what
Use a keyset-backed cursor if you need…
- Stable performance at deep pages and large tables. Offset pagination slows as OFFSET grows; keyset queries read only the next N rows (Pagination guidelines).
- Deterministic navigation (no duplicates/gaps) by ordering with a unique tie-breaker (for example, add id to a timestamp sort) (Keyset pagination).
- Next/previous/first/last navigation rather than page numbers. Keyset does not support random access by numeric page (Pagination guidelines).
Offset pagination may be acceptable if…
- You have small datasets and shallow paging, or an admin-only UI.
- You can tolerate count(*) overhead and page-number UX. GitLab documents count scalability limits and mitigations (for example, hiding totals) (Pagination guidelines).

Design notes that make cursor pagination work
- Choose a deterministic ORDER BY, and include a unique tie-breaker if your first sort isn’t unique (for example, ORDER BY created_at DESC, id DESC). The GitLab library auto-adds primary key ordering in common cases (Keyset pagination).
- Index coverage matters: create a composite index matching your ORDER BY so the database can fetch the next N rows without sorting (Pagination guidelines).
- Prefer Link headers to publish next and previous URLs so clients don’t construct cursors or query strings by hand (Pagination guidelines; MDN Link header).
- Expect no page numbers. Communicate this in the UI, and expose first/next/previous/last actions instead (Pagination guidelines).
Migrating from page numbers to a keyset-backed cursor
- Hide hard page counts. Prefer a capped counter (for example, show “1000+”) so removing COUNT(*) later doesn’t break UX (Pagination guidelines).
- Add a keyset ORDER BY with a stable tie-breaker and a covering index.
- Return next/previous relations via Link headers, where the URLs include an opaque cursor parameter generated by the server (Pagination guidelines; MDN Link header).
- Keep offset-based paging temporarily for backward compatibility if existing clients depend on it; removing it can be a breaking change (Pagination guidelines).
Related reading: Types of pagination in REST APIs and What is API versioning? Practical guidance for small teams.
Put this into practice
Draw the cursor representation and the database continuation condition separately. Then test a cursor under changed filters; an opaque token alone does not prove that the query uses keyset pagination.
Roll out a keyset-backed cursor safely
FAQ
Can keyset pagination support previous/first/last?
Yes. GitLab’s keyset paginator exposes cursor_for_next_page, cursor_for_previous_page, cursor_for_first_page, and cursor_for_last_page, making bidirectional navigation straightforward (Keyset pagination | GitLab Docs).
How do I expose the next page?
Include a Link header with a rel="next" URL that already includes the cursor produced by the server; clients should follow that URL rather than reconstructing it (Pagination guidelines; MDN Link header).