1Step-by-Step Breakdown
Why Every List Endpoint Needs Pagination. Returning an entire table's worth of rows in a single response — even one that starts small — becomes a genuine problem as data grows: slower queries, larger response payloads, and higher memory usage on both server and client. Pagination is a default requirement for any list endpoint, not an optional optimization added later.
Offset-Based Pagination: The Familiar Pattern. Offset pagination uses LIMIT and OFFSET (or their ORM equivalents) — the client requests a specific page number, and the server skips (page - 1) * pageSize rows before returning the next pageSize — simple to implement and matches the familiar "page 1, 2, 3..." UI pattern users recognize.
The Performance Problem: OFFSET Doesn't Scale. A database executing OFFSET 100000 must still scan and discard the first 100,000 matching rows before returning the requested page — the further into a large dataset a client pages, the slower the query becomes, since the database's work grows with the offset, not just the page size.
The Consistency Problem: Shifting Data Mid-Pagination. If a new row is inserted (or an existing one deleted) between a client fetching page 1 and page 2, offset pagination can cause a row to be skipped entirely or shown twice — since "offset 20" refers to a different set of rows depending on exactly when the query runs relative to concurrent writes.
Returning Total Count: A Related, Separate Cost. Displaying "Page 3 of 47" requires a separate COUNT(*) query, which itself can be expensive on a large, filtered table — a common optimization skips the exact count for very large result sets, showing "many more results" or an approximate count instead of an exact one.
When Offset Pagination Is Still the Right Choice. Despite its limitations, offset pagination remains appropriate for smaller datasets, admin interfaces where jumping directly to "page 15" is genuinely useful, or any context where the moderate performance cost at realistic scale is acceptable and the simpler client-side UX (arbitrary page-number jumping) outweighs cursor pagination's benefits.
Consistent, Explicit Ordering Is Mandatory. Pagination without an explicit, deterministic ORDER BY clause on a unique or near-unique column produces undefined row ordering between requests — the database is free to return rows in any order it finds convenient, meaning two consecutive page requests aren't even guaranteed to be consistent relative to each other at all.
Why does offset-based pagination become progressively slower for pages deep into a large dataset (e.g. page 5,000), compared to page 1?
- →The database must scan and discard every row before the offset, and that work grows with the offset value itself
- →Later pages always require more network round-trips to the client
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Fast, Consistent Pagination Prevents Confusing Content Shifts for Users Navigating Sequentially
A user relying on assistive technology to navigate through paginated results sequentially (rather than visually scanning) is more likely to notice and be confused by a duplicated or skipped item caused by offset pagination's consistency limitation, since they may have less visual context to immediately recognize the anomaly.
SEO Implications
- 1
Slow, Deep-Offset Pagination Queries Can Degrade Server Response Time for Large Catalogs
A large product catalog or content listing using offset pagination can suffer measurably slower response times on deep pages as the dataset grows, directly affecting server response time and page-speed-related search ranking signals for those specific pages.
Best Practices
Always specify an explicit, deterministic ORDER BY on a unique or near-unique column for any paginated query
Without this, row ordering between requests is undefined, undermining pagination's basic consistency guarantee even before considering concurrent writes.
Consider skipping an exact total COUNT for very large result sets, using an approximate or capped count instead
An exact count query can be nearly as expensive as the paginated query itself on a large, filtered table, and an approximate "1000+ results" is often sufficient for the actual UI need.
Frequent Bugs
A frequently-updated, paginated list occasionally shows the same item twice across two consecutive page requests, or skips an item entirely.
This is an inherent limitation of offset-based pagination under concurrent writes — a row inserted or deleted between two page fetches shifts every subsequent row's effective offset position. Consider cursor-based pagination (covered separately) if this consistency issue is unacceptable for the specific use case.
Real-World Examples
Diagnosing a Slow Admin Dashboard on Deep Pagination
An internal admin dashboard listing orders performed acceptably on early pages but became noticeably slow — several seconds — when an operator paged deep into historical data (page 2000+). Investigation confirmed the query's OFFSET value was the direct cause, since the database had to scan and discard 40,000 rows before returning each deep page. Since the admin use case genuinely needed the ability to jump to an arbitrary page number, the team added a database index specifically supporting the pagination's ORDER BY column, which meaningfully improved but didn't eliminate the fundamental offset-scaling cost — ultimately accepted as a reasonable tradeoff given the admin-only, lower-traffic context.
// Index added to support the ORDER BY, improving but not eliminating the cost
CREATE INDEX idx_orders_created_at ON orders(created_at);