CMD Guide
HomeSystem DesignScalable Systems (Advanced Topics)

What are the Main API Pagination Strategies offset, cursor, keyset, and When Should I Use Each

The three main API pagination strategies are offset pagination, cursor-based pagination, and keyset pagination, each suited for different scenarios (offset for simple page numbering, cursor for dynamic data feeds, and keyset for high-performance on large sorted data).

In other words, offset uses numeric page indices, cursor uses a pointer/token to the last retrieved item, and keyset (seek method) uses a specific field value (like an ID or timestamp) to determine the next page.

Choosing the right method depends on data size, how frequently the data changes, and whether users need to jump to specific pages.

Understanding API Pagination and Its Importance

Pagination is the practice of dividing a large set of results into smaller “pages” that clients can request one at a time.

This is critical for performance and usability: it reduces payload size (so responses load faster) and protects the backend from heavy queries by only fetching a limited number of records per request.

A well-designed pagination scheme also improves the developer experience by making API responses predictable and easier to navigate (often providing metadata like total count or next-page links).

The three common pagination patterns, offset, cursor, and keyset have unique trade-offs in simplicity, performance, and consistency.

Below, we explore each strategy and when to use it.

Offset-Based Pagination (Page Number Pagination)

Offset pagination (aka page-based pagination) is the simplest and most traditional approach. The client requests a specific page or offset index, and the server returns results starting from that position.

Offset Pagination
Offset Pagination

For example, GET /items?limit=10&offset=20 would retrieve items 21–30 (assuming offset starts at 0), effectively the 3rd page of results if each page has 10 items.

This method maps cleanly to user-visible page numbers.

Cursor-Based Pagination (Token or Cursor Pagination)

Cursor pagination uses a pointer (cursor) to keep track of your position in the dataset, rather than a page number.

The server provides an opaque cursor (often a string token) in each response, which the client sends back to request the next page.

Cursor Based Pagination
Cursor Based Pagination

For example, a response might include "next_cursor": "aBcDeFg123" and the client’s next request would be GET /items?limit=10&cursor=aBcDeFg123 to get the following items.

Internally, this cursor corresponds to the last item seen in the previous page.

Keyset Pagination (Seek Method)

Keyset pagination is a specific type of cursor-based approach that uses the actual values of a sorted key (or keys) to fetch the next page, rather than an arbitrary token.

In essence, the last seen record’s key (like an ID or timestamp) serves as the “cursor.”

Keyset Pagination
Keyset Pagination

For example, if you’re sorting by ID, and the last item on page 1 has id = 50, then page 2 can be fetched with a query like WHERE id > 50 LIMIT 10 to get the next 10 items.

Keyset paging is often called the “seek method” because it lets the database seek directly to the position of the last key, using an index, instead of scanning offset rows.

Check out REST API interview questions.

Choosing the Right Pagination Strategy (When to Use Each)

Choosing between offset, cursor, and keyset pagination depends on your use case and priorities.

Here’s a quick guide to when each strategy makes sense:

It’s worth noting that these strategies are not mutually exclusive.

In practice, many systems use a combination: for example, an internal admin API might offer offset pagination with total counts, while a public-facing API for the same data uses cursor or keyset for efficiency.

The key is to align your choice with how the data is used.

Understanding these trade-offs early will help you design an API that scales well and provides a good user experience without needing a painful retrofit later.

The cost model, and hardening cursors

The reason to prefer keyset at scale is a concrete cost difference, not a style preference. On a 10-million-row table with page size 50, LIMIT 50 OFFSET 500000 makes the database walk and discard about 500,000 index entries before returning a single row — hundreds of milliseconds that grow linearly with depth — whereas the keyset form WHERE (created_at, id) > (?, ?) ORDER BY created_at, id LIMIT 50 is an index seek to the last-seen key, roughly a couple of milliseconds at any depth. The tell in production is p95 latency plotted against page number: a flat line means you are seeking, a rising slope means you are still offsetting.

Two practical points the happy path skips. Keyset needs a total order: if the sort column is not unique, append a unique tiebreaker (the primary key) to the cursor tuple — otherwise a duplicate value straddling a page boundary silently drops or repeats a row. And because a cursor encodes a seek position, treat it as untrusted input: sign it (for example with an HMAC) or encrypt it so a client cannot forge a cursor that seeks into data it should not reach or that triggers a pathological scan. Offset needs neither, which is part of why it survives on small admin UIs where "jump to page 7" and a total count matter more than deep-scan cost.

Drift, traced

Newest-first list, page size 3. Rows at t0 (newest to oldest): [J, I, H, G, F, E].

  1. Page 1 (OFFSET 0 LIMIT 3) returns J, I, H.
  2. Two new rows K, L arrive at the head. The list is now [L, K, J, I, H, G, F, E].
  3. Page 2 (OFFSET 3 LIMIT 3) skips L, K, J and returns I, H, G — the user sees I and H twice, and never notices K and L exist mid-scroll (they landed on a "page 1" the user already passed). With deletes, the same positional shift silently skips rows instead.

Now keyset on the identical data: page 1 ended at H, so page 2 is WHERE (created_at, id) < (H.created_at, H.id) ORDER BY created_at DESC, id DESC LIMIT 3, which returns G, F, E — no duplicate, no gap. Inserts at the head cannot shift the window, because the window is anchored to a row's key, not to a row count.

Drill ladder

L1: "Just make the cursor the last row's id — done, right?"
Trap: "id alone is enough; it's unique."
Bar: id alone only works when you are sorting by id. For a created_at sort you need the (created_at, id) tuple: if the sort column is non-unique, a duplicate timestamp straddling a page boundary silently drops or repeats a row — the tiebreaker is what restores a total order.

L2: "Why not expose last_id directly in the URL? It's simpler than an opaque token."
Trap: "Opaque cursors are just ceremony."
Bar: A raw key invites forging and enumeration and welds every client to your schema and sort key. An opaque, signed (HMAC) cursor keeps the seek position tamper-evident and lets you change sort keys or add tiebreakers later without breaking clients.

L3: "The product wants 'Page 7 of 50' and total counts — so keyset is out?"
Trap: "Total counts force offset pagination."
Bar: Serve an estimated or cached count (planner estimate, periodic COUNT(*)) alongside keyset pages; an exact COUNT(*) on every page re-scans exactly what keyset saved you. Reserve true offset paging for the small admin datasets where the scan cost is negligible.

🤖 Don't fully get this? Learn it with Claude

Stuck on What are the Main API Pagination Strategies offset, cursor, keyset, and When Should I Use Each? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **What are the Main API Pagination Strategies offset, cursor, keyset, and When Should I Use Each** (System Design) and want to truly understand it. Explain What are the Main API Pagination Strategies offset, cursor, keyset, and When Should I Use Each from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **What are the Main API Pagination Strategies offset, cursor, keyset, and When Should I Use Each** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **What are the Main API Pagination Strategies offset, cursor, keyset, and When Should I Use Each** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **What are the Main API Pagination Strategies offset, cursor, keyset, and When Should I Use Each** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes