API Pagination Testing: How to Catch Missing and Duplicate Records
Test offset and cursor pagination for gaps, duplicates, sorting problems, and changes while clients traverse a result set.
API pagination testing checks that clients can move through a collection without silently skipping or repeating records. Test the API's documented pagination contract with a known dataset, a deterministic sort, and assertions across the full traversal. Offset and cursor designs behave differently when data changes, so decide what consistency the API promises before calling a changing result set a bug.
Start with the pagination contract
Find the documented request parameters, default and maximum page size, response envelope, ordering rules, and continuation mechanism. A common offset request is `GET /v1/orders?limit=3&offset=0`; a cursor API might use `GET /v1/orders?limit=3&after=eyJpZCI6MTAwfQ`. Responses often include `items` and either a `next` link, a next cursor, or a total. These are conventions, not universal HTTP requirements.
Use a disposable dataset with more records than one page. Give every record a unique ID and a sortable timestamp. Record the expected IDs before testing, then compare the combined results with that set. Check order as well as membership: a set comparison alone cannot detect a changed sort order.
Offset versus cursor pagination
Offset pagination asks the server to skip a count of rows. It is simple to inspect and useful for numbered pages, but large offsets can be expensive. More importantly, inserts before the current offset can shift rows: page two may repeat an item from page one. Deletions before the offset can shift rows the other way and cause a skipped item.
Cursor pagination asks for records after a position represented by a token. A cursor can encode a key, timestamp, or opaque server state. Keyset cursors often behave better for long traversals because they continue relative to an ordering key rather than a row count. They still need a stable order and have contract-specific behavior for deleted records, updated sort keys, and newly inserted records.
Neither style guarantees a frozen snapshot by itself. Some APIs provide snapshot or transaction semantics; many return a moving view. Document what callers should expect.
Make ordering deterministic
Sorting only by a non-unique field such as `created_at` leaves ties ambiguous. If several rows have the same timestamp, the database can return them in either order, and page boundaries can split ties inconsistently. Add a unique tie-breaker, for example `ORDER BY created_at ASC, id ASC`, and test that the API preserves it across every page.
If clients can choose sort fields, test each supported field and direction. Reject or safely handle unknown sort keys according to the API contract; never assume arbitrary user input should become a database column name.
Exercise the boundaries
Use a small page size to expose boundary errors, then check the documented maximum. Test the first page with no cursor or offset, a middle page, and the last page where the continuation marker should indicate completion. Ask for a page beyond the end and verify the documented empty result shape. An empty collection should also return a valid response, usually with an empty `items` array, but the exact envelope belongs to the API contract.
Try page size zero, a negative number, a non-integer, and a value above the maximum. The server may reject, clamp, or default these values; assert its published rule. Test a very large offset carefully in a test environment and verify bounded behavior without creating an accidental expensive production query.
Test cursors as untrusted input
If cursors are opaque, clients should pass them back unchanged. Check that a valid cursor advances correctly, and that malformed, truncated, modified, expired, or context-mismatched cursors receive the documented error behavior. A cursor copied from a different filter or tenant should not quietly expose unrelated rows. If the token is signed or encrypted, alteration should be detected server-side.
Example traversal:
GET /v1/orders?limit=2&sort=created_at,id HTTP/1.1
Host: api.example.test
Authorization: Bearer TEST_TOKEN
Accept: application/json{"items":[{"id":"o-101","created_at":"2026-10-01T10:00:00Z"},{"id":"o-102","created_at":"2026-10-01T10:00:00Z"}],"next_cursor":"cD0y"}Then send the returned cursor as provided:
GET /v1/orders?limit=2&sort=created_at,id&after=cD0y HTTP/1.1
Host: api.example.test
Authorization: Bearer TEST_TOKEN
Accept: application/jsonDo not construct a cursor by guessing its internals unless the API explicitly defines that format.
Combine filters and pagination
Filtering and sorting must apply consistently to every page. For a request such as `GET /v1/orders?status=paid&sort=created_at,id&limit=2`, verify that every returned record is paid, records satisfy the sort, and the traversal includes every matching test record exactly once. Change one filter between requests and make sure the API does not reuse a cursor in a way that returns a confusing mixture. Check authorization and tenant filters across all pages too.
When a record is inserted or deleted during traversal, results depend on the API's consistency model. To test snapshot behavior, hold a stable fixture and compare against its initial IDs. To test a live view, deliberately insert and delete around the page boundary and verify the documented behavior, such as possible movement without promises of a snapshot. Do not label a moving collection inconsistent if the contract promises only best-effort traversal.
A repeatable testing checklist
- [ ] Confirm default size, maximum size, sort syntax, response fields, and consistency guarantees.
- [ ] Traverse an empty collection, one-page collection, and multi-page collection.
- [ ] Assert first, middle, and last page behavior and the response shape beyond the end.
- [ ] Compare all returned IDs with the expected IDs; detect duplicates and omissions.
- [ ] Use tied sort values and verify a unique tie-breaker keeps order stable.
- [ ] Test zero, negative, non-numeric, and over-limit page sizes per contract.
- [ ] Try malformed, expired, altered, and wrong-context cursors safely.
- [ ] Combine filters, sort directions, and pagination; verify every result meets all conditions.
- [ ] Insert and delete records during traversal only in a controlled test environment.
- [ ] Repeat with a different tenant or permission level and check for data leakage.
For general request assertions, see how to test REST APIs and API testing strategies. To check how request changes affect releases, read API regression testing.
References
Use API Test Lab to send the same first-page and continuation requests and compare the response bodies against your contract.
More from the blog
Read 3 related articles from our latest posts.