Fundamentals/API Design

API Pagination

API Design

Visual Representation

Rendering diagram...

What is it?

📖 Think of it like reading a book:

You don't read all 500 pages of a book at once. You read one page at a time, turn to the next when ready. A book without page numbers would be chaos — how would you tell someone "I'm on page 47"?

API Pagination is the same — instead of returning ALL 10 million users in one response, the API returns them in "pages" (small chunks). "Here are users 1-20. Want more? Ask for the next page."

Without pagination: your API returns 10 million records → 2 GB response → mobile app crashes → server runs out of memory → everyone has a bad day.

💡 Simple Summary: Return data in small chunks (pages) instead of all at once. Saves bandwidth, memory, and prevents timeouts.

How it works — Like you're watching it happen

Three main pagination approaches:

1. Offset-Based (Simple but flawed):

  • Client says: "Give me 20 items starting from position 40" → GET /posts?limit=20&offset=40
  • Server does: SELECT * FROM posts LIMIT 20 OFFSET 40
  • Client shows page 3 (items 41-60).
  • Problem: If a new post is added while you're browsing, items shift and you might see duplicates or miss items!
  • 2. Cursor-Based (Better, used by most big apps):

  • Server returns: { data: [...20 posts...], next_cursor: "abc123" }
  • Client says: "Give me the next 20 after cursor abc123" → GET /posts?limit=20&cursor=abc123
  • Server does: SELECT * FROM posts WHERE id < cursor_id LIMIT 20
  • The cursor is like a BOOKMARK — "I was reading up to here, continue from this point."
  • Doesn't matter if new items are added — your bookmark stays valid!
  • 3. Keyset/Seek Pagination:

  • Similar to cursor but uses the last seen value directly: GET /posts?created_after=2024-01-15T10:30:00&limit=20
  • Server: SELECT * FROM posts WHERE created_at > '2024-01-15T10:30:00' ORDER BY created_at LIMIT 20
  • But wait — why not just use offset? It's simpler.

    Offset has two big problems: (1) Performance — OFFSET 1000000 means the database scans and SKIPS 1 million rows before returning 20. Gets slower as pages increase. (2) Consistency — If items are added/deleted between page loads, you get duplicates or missing items. Cursor-based pagination avoids both by always starting from a fixed point.

    Why should you care? (Interview perspective)

  • 🎯 "How would you paginate this feed?" is asked in Design Instagram, Twitter, YouTube interviews
  • Offset vs cursor is a CLASSIC interview differentiator between junior and senior answers
  • Shows you understand performance at scale (millions of records)
  • Connects to database indexing and query optimization discussions
  • Key Things to Remember

  • Offset: simple but bad at scale — Easy to implement, easy to understand. But OFFSET 10000000 is catastrophically slow (database must scan all skipped rows).
  • Cursor: industry standard — Instagram, Twitter, Slack all use cursor-based. Fast at any page depth (always indexed lookup). No skipping.
  • Never return unbounded results — Always have a DEFAULT limit (20-100). Never let clients request limit=999999.
  • Include total count carefully — "Total: 5 million results" requires a COUNT(*) query which is SLOW on large tables. Consider approximate counts or skip total.
  • Cursor should be opaque — Client shouldn't know the cursor is a timestamp or ID. Use base64-encoded strings so you can change implementation without breaking clients.
  • Sort order affects pagination — If sorting by "popularity," cursor needs to encode both popularity AND id (for ties). Multi-column cursors.
  • Response format — { data: [...], pagination: { next_cursor: "abc", has_more: true } }
  • Forward-only limitation — Cursor pagination often only goes forward. Going backward needs a "previous_cursor" or a different approach.
  • Infinite scroll = cursor — When you scroll Instagram/Twitter endlessly, that's cursor-based pagination loading the next "page" as you scroll.
  • Deep pagination defense — Even with offset, limit to first 1000 pages: "For results beyond page 1000, please refine your search." Google does this!
  • Real Examples You Use Daily

    📱 Instagram feed — Cursor-based. As you scroll, the app sends the cursor of the last post you saw. Server returns the next batch. Works perfectly even with new posts being added constantly.

    🐦 Twitter timeline — Uses cursor pagination. The response includes a "next_cursor" token. Client sends it to get older tweets. This is why your timeline position doesn't jump around when new tweets arrive.

    🔍 Google Search — Technically offset-based (?start=10 for page 2) but LIMITS to ~30 pages. Try going to page 50 of Google results — you can't! They say "refine your search." Avoids deep offset performance problems.

    🛒 Amazon product listings — Uses page numbers (offset-style) for user experience (people like clicking "page 5"). But behind the scenes, it's optimized with elasticsearch cursors and limited to reasonable page depths.

    Common Mistakes in Interviews

    Only knowing offset pagination — "I'll use LIMIT and OFFSET." Interviewer: "What happens at page 50000?" If you don't know cursor-based, you'll struggle.

    Returning ALL data then paginating client-side — "Server returns everything, frontend shows 20 at a time." NO! The whole point is to reduce what the server sends. This wastes bandwidth and memory.

    Not handling concurrent modifications — With offset, items inserted/deleted between requests cause duplicates or gaps. Mention this problem and how cursors solve it.

    Forgetting to include pagination metadata — Just returning data without "has_more", "next_cursor", or "total" leaves the client guessing if there are more pages.

    Using auto-increment IDs as cursors directly — Exposing internal IDs in cursors leaks information (competitors can estimate your growth). Use opaque encoded cursors.

    🎯 Interview One-Liner

    "I'd use cursor-based pagination for feeds and timelines because it provides consistent results regardless of concurrent inserts, O(1) database performance at any depth using indexed lookups, and naturally supports infinite scroll — unlike offset which degrades linearly and suffers from data drift."

    Interview Q&A

    Q: Offset pagination — what's wrong with it at scale?

    Two problems: (1) Performance: OFFSET 1000000 forces the DB to scan and discard 1M rows. Gets slower with every page. (2) Consistency: user scrolls to page 5, then a new item is inserted at the top — now page 5 shows an item that was already on page 4 (duplicate). Or an item gets deleted — one item is never seen (gap). Cursor avoids both by always querying relative to a fixed point.

    Q: How does cursor-based pagination work internally?

    The cursor encodes the last item's sort value (e.g., created_at timestamp + ID for tiebreaking). Next query: SELECT * FROM posts WHERE (created_at, id) < (cursor_timestamp, cursor_id) ORDER BY created_at DESC, id DESC LIMIT 20. This uses an index directly — no row scanning. The cursor is base64-encoded and opaque to clients.

    Q: How do you paginate when sorting by a non-unique field like "likes"?

    You need a tiebreaker. Cursor = (likes_count, id). Query: WHERE (likes, id) < (95, 456) ORDER BY likes DESC, id DESC. Without the tiebreaker, items with the same number of likes would be randomly split across pages. Always compound your cursor with a unique field.

    Q: Should you include total count in pagination response?

    It depends. COUNT(*) on a table with 100M rows is SLOW (full table scan in PostgreSQL). For feeds (Instagram, Twitter), you DON'T show total count — just "has_more: true." For search results or admin dashboards where users expect totals, either (1) use approximate counts (pg_class.reltuples), (2) cache the count, or (3) limit counting ("1000+ results").

    Q: Infinite scroll vs page numbers — which uses which?

    Infinite scroll (Instagram, Twitter) → cursor-based. Natural fit: "load more from where I left off." Page numbers (Google, e-commerce) → typically offset or keyset. Users want to jump to "page 5" directly. But even with page numbers, you can use keyset under the hood for performance — just need to maintain the mapping.

    Quick Quiz

    1/5

    Instagram shows infinite scroll — you keep scrolling and new posts load. Which pagination type powers this?