API Pagination
API Design
Visual Representation
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):
2. Cursor-Based (Better, used by most big apps):
3. Keyset/Seek Pagination:
❓ 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)
Key Things to Remember
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/5Instagram shows infinite scroll — you keep scrolling and new posts load. Which pagination type powers this?