Seven Pagination Traps That Turn a Clean API Into a Support Queue
Photo: NASA Open Innovation Team, Public domain, via Wikimedia Commons
Pagination is one of those API details that feels settled until it is not. A small dataset hides every flaw. A growing dataset exposes them all at once: duplicate records, missing rows, timeouts, and support tickets that describe the problem as the API being unreliable. In reality, the API may be behaving exactly as designed. The design is just hostile to real-world data.
Here are seven pagination traps that turn a clean API into a support queue, along with fixes that improve performance, consistency, and developer experience.
1. Unstable Sort Order
The most common pagination bug is not in the pagination parameters; it is in the sort. If the query orders by a non-unique column such as created_at or status, rows with equal values can move between pages as the database chooses different plans or as new records arrive. Clients see duplicates on one page and missing records on another.
The fix is to enforce a deterministic tiebreaker. Sort by the primary key or another unique field in addition to the business sort. Document the full ordering, not just the visible column. A stable order is the foundation of every pagination strategy.
2. Deep Offset Windows
Offset pagination is easy to explain and easy to implement. It is also increasingly expensive as the offset grows. A query for page 500 asks the database to scan and discard thousands of rows before returning the page. Under load, those deep pages become timeout generators.
The fix is to use keyset pagination for large or frequently traversed datasets. Instead of asking for an offset, the client asks for records after a known cursor. The database can use an index and return a page without scanning everything before it. Offset pagination can remain for small, bounded collections where simplicity matters more than scale.
3. Total Counts as a Comfort Blanket
Many APIs return a total count with every page because clients like progress indicators. But counting a large filtered dataset can be as expensive as the query itself, and the number is often stale by the time the client renders it. Worse, clients sometimes build logic that depends on the count remaining stable across pages.
The fix is to make counts optional or approximate. Offer a separate count endpoint with its own caching and freshness contract. If the UI needs progress, use has_more or next_cursor rather than a precise total. A count is a reporting feature, not a pagination primitive.
4. Opaque Cursors With No Expiry Contract
Cursors are useful because they encode position without exposing database offsets. But an opaque cursor with no documented lifetime is a trap. Clients store cursors, resume jobs later, and then receive unexplained errors when the cursor expires or the underlying data changes.
The fix is to define cursor behavior explicitly. State whether cursors are stable across data changes, how long they remain valid, and what error clients should expect when they expire. If cursors are tied to a snapshot, say so. If they are not, clients need to know how to restart safely.
5. Page Size as an Afterthought
A default page size that is too small creates chatty clients and unnecessary load. A maximum that is too high invites accidental full-table scans. A page size that changes silently between versions breaks client assumptions.
The fix is to publish a default and a maximum, enforce both, and keep them stable across minor versions. Allow clients to request smaller pages, but do not let them exceed the maximum. If the service needs to reduce page size under pressure, return a clear signal rather than silently truncating results.
6. No Consistency Story Across Pages
Pagination over a changing dataset is inherently tricky. New records can arrive, old records can be deleted, and sort values can be updated. If the API does not state what consistency clients should expect, every page boundary becomes a potential surprise.
The fix is to choose a consistency model and document it. Some APIs offer snapshot pagination, where the cursor represents a fixed view. Others offer read-committed pagination, where changes may appear between pages. Neither is universally correct, but clients need to know which one they are using. For critical exports, snapshot semantics are usually worth the storage cost.
7. Links and Errors That Force Client Guesswork
A pagination response that returns only an array of items forces every client to reconstruct the next request. A response that returns an error with no explanation forces every client to guess whether the cursor is invalid, expired, or simply empty.
The fix is to include explicit pagination metadata: next_cursor, has_more, and where appropriate previous_cursor. Use clear error codes for invalid or expired cursors, and include a remediation hint. Good pagination is not just about moving through data; it is about making the next step obvious.
The Common Thread
Every pagination trap comes back to the same principle: the API must be predictable under real data and real client behavior. Stable ordering, bounded queries, honest counts, explicit cursor contracts, and clear metadata are not luxury features. They are the difference between an API that scales quietly and one that generates a support queue every time a dataset grows.
Teams building for 2026 should treat pagination as part of the public contract, not an internal query detail. The clients will notice. So will the support team.