The Hidden Problem with Offset-Based Pagination
Pagination with limit & offset
Pagination is one of those problems that feels solved early in a developer’s career.
LIMIT and OFFSET are simple, expressive, and map nicely to the idea of "pages" in a UI.
That is, until your database grows...
When datasets reach tens or hundreds of millions of rows, offset-based pagination quietly becomes a performance liability — especially under filtering and sorting. This article explains why that happens, and how cursor-based pagination solves it.
The Familiar Approach: OFFSET-Based Pagination
Most applications start with queries like this:
SELECT *
FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;
Page 1 is fast. Page 2 is still fast. Everything seems fine — until users request deeper pages.
LIMIT 20 OFFSET 500000;
At scale, this query can become significantly slower than the first page.
This is not accidental. It’s a direct consequence of how databases execute OFFSET.
And no, this problem is not unique to RDBMSs. The exact same class of problem exists in many NoSQL systems — including MongoDB and Elasticsearch — although it shows up under slightly different names and mechanics.
Why OFFSET Gets Slower as the Page Number Grows
OFFSET N does not mean "jump directly to row N".
Internally, most relational databases:
- Apply the
WHEREfilter - Sort the matching rows (or traverse a sorted index)
- Scan and discard the first N rows
- Return the next
LIMITrows
So even though the client only wants 20 rows, the database may need to:
- Read hundreds of thousands or millions of rows
- Throw most of them away
This means:
- Page 1 reads ~20 rows
- Page 10,000 reads ~200,000 rows
- Page 50,000 reads ~1,000,000 rows
The cost grows linearly with the offset.
Even with good indexing, this behavior remains — the database still has to walk the index entry by entry. That’s why OFFSET-based pagination does not scale well for large datasets.
The Real Problem: We’re Asking the Wrong Question
Offset-based pagination answers this question:
"Give me page number N."
But databases are much better at answering a different question:
"Give me the next rows after this one."
That shift in thinking leads directly to cursor-based pagination.
Cursor-Based Pagination (a.k.a. Keyset Pagination)
Cursor pagination replaces OFFSET with a stable, ordered reference point — the "cursor".
Instead of skipping rows, the query continues from the last seen value.
First Page
SELECT *
FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
The client stores the last row’s created_at value.
Next Page
SELECT *
FROM orders
WHERE status = 'paid'
AND created_at < '2025-01-01T12:34:56'
ORDER BY created_at DESC
LIMIT 20;
The database now performs an index range scan — no rows are skipped, no work is wasted.
Why Cursor Pagination Scales
- No row skipping
- No growing scan cost
- Query time remains constant
- Perfect alignment with B-tree indexes
The database always starts exactly where it should.
This makes cursor pagination ideal for:
- Very large tables
- High-traffic APIs
- Infinite Scroll or "Load more" experiences
- Time-ordered data (events, logs, messages, orders)
A Perfect Fit: Activity Feeds and Timelines
Consider a system showing:
- User activity
- Audit logs
- Notifications
- Orders history
Users:
- Always move forward
- Rarely jump to page 37
- Expect consistent performance
Cursor pagination matches this usage pattern perfectly:
- "Give me the next 20 items"
- Stable ordering
- Predictable latency
This is why cursor pagination is used by companies like Twitter, GitHub, Stripe, and Facebook.
When Cursor Pagination Is Not a Good Fit
Cursor pagination is not a silver bullet. Its trade-offs are real.
1. You Can’t Jump to Arbitrary Pages
There is no "page 42" without walking through the previous cursors.
If your UI requires:
- Random page access
- Direct navigation to specific page numbers
OFFSET may still be necessary (or a hybrid approach).
2. Requires a Stable, Unique Sort Key
You need a column (or composite) that is:
- Indexed
- Immutable
- Unambiguous
Common choices:
idcreated_at + id
If ordering is unstable, pagination breaks.
3. More Complex Client Logic
The client must:
- Store cursors
- Pass them back to the server
- Handle edge cases when data changes
This complexity is manageable — but it’s not free.
Conclusion
Offset-based pagination is simple and intuitive — and perfectly fine for small datasets.
But at scale, it becomes increasingly inefficient because the database must scan and discard more rows as offsets grow.
Cursor-based pagination flips the model:
- Instead of skipping rows, it continues from a known position
- Performance remains constant
- The database does exactly the work required — no more, no less
For large datasets, high traffic, and sequential access patterns, cursor pagination isn’t just an optimization — it’s the correct architectural choice.
And that’s what your interviewer was looking for.
If you want, I can:
- Add SQL execution plan comparisons
- Show composite cursor examples
- Rewrite this as a system design interview answer
- Or adapt it specifically to PostgreSQL / MySQL / Elasticsearch