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?