GUIDE / api
How to Test Pagination in an API
Learn how to test pagination in an API: page vs cursor models, limit bounds, empty pages, stable ordering, and a practical checklist for list endpoints.
Missing rows, duplicate infinite-scroll cards, and page 3 leaking another tenant's data rarely show up in a single status === 200 list call. How to test pagination in an API separates shallow list checks from production-ready coverage of page size, cursors, filters, auth, and full-collection integrity.
This guide covers pagination models, a full test design method, boundary cases, consistency under concurrent writes, performance notes, worked examples for page/limit and cursor APIs, and common mistakes. Build the cases with the discipline from how to write API test cases, and ground HTTP expectations with REST API status codes.
Why Pagination Testing Is High Value
List endpoints are:
- High traffic
- Easy to break with sort/filter combinations
- Sensitive to data races
- Central to UI performance
- Frequent sources of off-by-one defects
A single page=2 bug can hide an entire day of orders from support tools. A cursor bug can trap a mobile app in an infinite loading loop. Treat pagination as a feature, not a query parameter afterthought.
Pagination Models You Will Meet
| Model | Typical params | Response hints | Common UI |
|---|---|---|---|
| Offset / limit | offset, limit | total, items | Admin tables |
| Page / per_page | page, per_page | totalPages, page | Classic pagination controls |
| Cursor | cursor, limit | nextCursor, hasMore | Infinite scroll feeds |
| Keyset | after_id, limit | next_after_id | Large ordered tables |
| Link header | often cursor/offset | Link: rel=next | GitHub-style APIs |
| GraphQL connections | first, after | pageInfo, edges | Modern app backends |
Your tests must match the model. Applying page-number assumptions to cursor APIs produces false failures and false confidence.
Core Invariants for Any Paginated List
Regardless of model, strong suites verify:
- Deterministic ordering when sort is specified (and documented default sort when not).
- No duplicates across a full walk of the collection for a stable dataset.
- No missing items across a full walk for a stable dataset.
- Page size honored up to documented maximums.
- Final page behavior is clear (partial list, empty list, or error).
- Invalid inputs fail predictably.
- Filters apply across all pages, not only page 1.
- Authorization applies across all pages, not only page 1.
Write those invariants as reusable helpers so every list endpoint does not reinvent checks.
Step-by-Step: How to Test Pagination in an API
Step 1: Read or reverse-engineer the contract
Capture:
- Parameter names and defaults
- Maximum page size
- Sort defaults
- Whether total counts exist
- Empty collection behavior
- Error codes for bad params
If docs are missing, use discovery techniques from the guide on how to test an API without documentation style workflows, then lock observations into cases.
Step 2: Seed controlled datasets
You need known cardinality.
Example seed plan for limit testing:
| Dataset | Size | Purpose |
|---|---|---|
| empty | 0 | empty state |
| single | 1 | single-item page |
| exact | 30 | exact multiple of limit 10 |
| partial | 25 | final partial page |
| large | 500+ | performance and deep pages |
Seed deterministic ids and sort keys (createdAt, sku) so expected order is calculable.
Step 3: Establish a baseline full dump
If the collection is small in test env, fetch with max allowed limit or iterate all pages once and store the ordered id list as expected inventory.
Expected IDs (createdAt asc):
[id1, id2, id3, ... id25]
All pagination walks must reconstruct the same multiset of ids for stable data.
Step 4: Test default pagination
Call the list endpoint with no page params.
Assert:
- Status 200 (or documented success)
- Default page size length (or fewer if total smaller)
- Presence of navigation metadata if promised
- Default sort order
Example:
GET /orders HTTP/1.1
Authorization: Bearer <token>
{
"data": [ ... 20 items ... ],
"page": 1,
"perPage": 20,
"total": 25,
"totalPages": 2
}
Step 5: Walk all pages and reconcile
For page/limit:
page=1, perPage=10 -> ids A
page=2, perPage=10 -> ids B
page=3, perPage=10 -> ids C
concat(A,B,C) == expected ids
intersection empty between A,B,C
For cursor:
1) GET /items?limit=10
2) GET /items?limit=10&cursor={next}
3) repeat until hasMore=false
Assert the concatenated ids match the baseline.
Step 6: Boundary test page sizes
| Input | Expected (example policy) |
|---|---|
| limit=1 | 1 item when data exists |
| limit=max | max items |
| limit=max+1 | 400 or clamped to max (document which) |
| limit=0 | 400 or empty data with 200 (document which) |
| limit=-1 | 400 |
| limit=abc | 400 |
| limit omitted | default |
Never assume clamp vs error. Observe, confirm, then lock.
Step 7: Boundary test page positions
For page model:
page=1first page- last page exact
- last page partial
page=totalPages+1empty data or 404 per contractpage=0or negative -> 400
For offset model:
offset=0offset=total-1offset=totalempty- huge offset empty or 400
Step 8: Sort stability and tie-breakers
If many rows share the same createdAt, unstable sort causes duplicates/misses across pages. Require a unique tie-breaker (id) in ordering.
Test:
- Create multiple rows with identical sort key if possible.
- Paginate with small limit.
- Confirm no duplicates/misses.
Step 9: Filters and search across pages
Apply a filter that matches 25 items. Paginate with limit 10. Ensure all pages honor the filter and concatenated results equal the filtered baseline.
Common bug: filter on page 1 only, or total count ignores filter.
Step 10: Authorization across pages
As user A, paginate resources. Confirm no user B rows appear on any page. Attempt cursors captured from user B (if obtainable in test) and expect 403/404/empty per security design.
Step 11: Mutation between pages
Simulate real users:
- Fetch page 1.
- Insert a new record at the top of default sort.
- Fetch page 2.
Document actual behavior:
- Offset pagination may duplicate or skip.
- Cursor/keyset often handles this better.
Your test may be observational rather than "must never shift" unless product promises snapshot isolation.
Step 12: Metadata consistency
If total, totalPages, hasMore, nextCursor exist, assert internal consistency:
if offset + count < total => hasMore true
if page < totalPages => next link present
if data.length == 0 and page == 1 => total == 0
Schema-check metadata objects using techniques from JSON Schema validation in API testing.
Worked Example: Page/Limit Orders API
Contract sketch:
GET /orders?page=&perPage=&status=
default perPage=20, max=100
sort: createdAt desc, id desc
response: { data, page, perPage, total, totalPages }
Seed
25 orders for buyer, createdAt ascending known.
Cases
| ID | Title | Steps | Expected |
|---|---|---|---|
| PG-001 | Default list | GET /orders | 200, 20 items, page=1, total=25, totalPages=2 |
| PG-002 | Page 2 partial | perPage=20&page=2 | 5 items, no overlap with page 1 |
| PG-003 | Full walk perPage=10 | pages 1-3 | 10+10+5 unique ids = all 25 |
| PG-004 | perPage max | perPage=100 | 25 items, totalPages=1 |
| PG-005 | perPage too large | perPage=101 | 400 with validation error |
| PG-006 | page beyond end | page=5&perPage=10 | 200 with data=[] OR documented error |
| PG-007 | filter paid only | status=paid across pages | only paid, totals match filtered count |
| PG-008 | invalid page | page=-1 | 400 |
Example automation
test("orders pagination walk has no gaps or duplicates", async () => {
const perPage = 10;
const all = [];
let page = 1;
let totalPages = 1;
do {
const res = await api.get(`/orders?page=${page}&perPage=${perPage}`, token);
expect(res.status).toBe(200);
expect(res.body.perPage).toBe(perPage);
all.push(...res.body.data.map((x) => x.id));
totalPages = res.body.totalPages;
page += 1;
} while (page <= totalPages);
expect(new Set(all).size).toBe(all.length);
expect(all.sort()).toEqual(expectedIdsSorted);
});
Worked Example: Cursor Feed API
Contract sketch:
GET /feed?limit=&cursor=
response: { data, nextCursor, hasMore }
Cases
| ID | Title | Expected |
|---|---|---|
| CR-001 | First page | hasMore true when more exist, nextCursor non-null |
| CR-002 | Walk until end | hasMore false, nextCursor null/absent, full unique set |
| CR-003 | Reuse exhausted cursor | empty data or same end behavior, no 500 |
| CR-004 | Tampered cursor | 400 |
| CR-005 | limit=1 walk | N requests for N items |
| CR-006 | Concurrent insert during walk | document skip/dup policy |
Cursor tests should never assume page semantics.
async function fetchAllCursor(path, token, limit = 10) {
const out = [];
let cursor = null;
let guard = 0;
while (guard++ < 1000) {
const q = cursor ? `?limit=${limit}&cursor=${cursor}` : `?limit=${limit}`;
const res = await api.get(path + q, token);
expect(res.status).toBe(200);
out.push(...res.body.data);
if (!res.body.hasMore) break;
cursor = res.body.nextCursor;
expect(cursor).toBeTruthy();
}
return out;
}
Comparison Table: What to Assert by Model
| Assertion | Page/offset | Cursor/keyset |
|---|---|---|
| Exact total count | Often yes | Sometimes no |
| totalPages math | Yes | No |
| nextCursor present when hasMore | No | Yes |
| Random access to page 5 | Yes | Usually no |
| Stability under inserts | Weaker | Stronger typically |
| Easy UI "jump to page" | Yes | No |
Performance and Abuse Angles
Pagination testing is not only functional.
- Deep offsets (
offset=1_000_000) may time out. Decide if that is acceptable. limit=100000may exhaust memory. Expect clamp or 400.- Unindexed sort fields plus pagination create slow queries.
- Scraping patterns walk all pages quickly: rate limits matter.
Add at least one performance observation test in staging for the heaviest list.
GraphQL Connection Notes
If you test GraphQL:
- Assert
pageInfo.hasNextPageandendCursorconsistency. - Confirm
edges.cursoradvances. - Test
first/lastand forward/backward pagination if both exist. - Ensure
nodeslength respectsfirst.
The invariants (no dups/misses on stable data) still apply.
Test Data Management Tips
- Isolate tenant data so totals are deterministic.
- Freeze clock if sort uses time and factories create collisions.
- Clean up between tests or use unique prefixes.
- Avoid depending on shared sandbox lists other teams mutate.
Checklist: Pagination Coverage
- Model identified (page, offset, cursor, GraphQL)
- Defaults documented and tested
- Min/max limit boundaries tested
- Empty collection tested
- Partial final page tested
- Full walk uniqueness and completeness tested
- Sort default and explicit sort tested
- Tie-breaker stability considered
- Filters apply on all pages
- Authz applies on all pages
- Invalid params return clear errors
- Metadata internal consistency checked
- Mutation-during-walk behavior noted
- Schema validation on envelope fields
- Rate limit / max page size abuse considered
Common Mistakes When Testing API Pagination
Mistake 1: Only checking page 1
Most pagination bugs appear on page 2+ or the last page.
Mistake 2: Asserting length == limit always
Last page is often shorter. Empty pages may be valid.
Mistake 3: Ignoring sort order
Without stable order, duplicate/miss checks are meaningless.
Mistake 4: Trusting total without reconciling data
total can lie while data is correct, or the reverse. Check both.
Mistake 5: Using production-sized data for unit-like API tests
Prefer controlled seeds for functional correctness; use large data in separate performance suites.
Mistake 6: Forgetting filtered totals
UI shows "3 results" while pages include unfiltered rows. Catch it.
Mistake 7: Treating cursor as offset
Opaque cursors may encode ids/timestamps. Do not invent arithmetic on them.
Mistake 8: Skipping security on later pages
IDOR and tenant leaks often show up when the first page is clean and later pages are not.
How Pagination Cases Fit a Broader API Suite
Pagination belongs in:
- Smoke: one list call with defaults
- Regression: full walk on critical resources
- Security pack: cross-user cursor/page access
- Performance pack: deep pages and max limits
Structure cases cleanly using patterns from the API testing tutorial.
Sorting, Filtering, and Pagination Together
Most production bugs appear at the intersection of sort, filter, and page navigation, not on a bare list call.
Composite test pattern
- Seed 40 orders across two statuses and mixed created times.
- Filter
status=paid(expect 22 paid). - Sort
createdAt asc, id asc. - Walk with
perPage=5. - Assert concatenated ids equal the filtered sorted baseline.
- Assert
totalequals 22 if totals are offered. - Change sort to
createdAt descand repeat uniqueness checks.
Search plus pagination
Search endpoints often paginate differently:
- Relevance sort may be unstable across replicas.
- Sparse results may return empty pages earlier than total suggests.
- Typo-tolerant search can change match sets after index rebuilds.
For search, store the result id set from a single full dump in one request when max limit allows, then compare paged walks immediately afterward to reduce environment drift. If relevance is non-deterministic, assert weaker invariants (all returned ids match the query predicate) instead of exact global order.
Multi-field filters
When filters combine (status, region, minTotal), generate a truth table of a few combinations and paginate each. Especially test "filter matches zero rows" and "filter matches exactly one page."
Consistency Models: What Product Must Promise
Testers should force an explicit product statement about consistency during pagination.
| Promise | Meaning | Test approach |
|---|---|---|
| Snapshot isolation | Pages reflect collection at first request | Hard to get without server support; rare |
| Continuous consistency | Each page reads latest data | Expect possible skips/dupes under writes |
| Keyset stability | Walk by key does not replay earlier keys | Cursor tests under inserts |
| Best effort | No strong promise | Document observed behavior, focus on no 500s |
If the UI shows page numbers and totals, users assume a semi-stable snapshot. If the UI is infinite scroll, users tolerate mild shifts but not endless duplicates. Align tests with the UX promise.
Idempotent Walks and Client SDK Tests
If you ship an SDK method like client.orders.iterateAll(), test that helper, not only raw HTTP.
Cases:
- Iterator stops on
hasMore=false - Iterator throws on 401 mid-walk
- Iterator does not skip page on transient retry (or does, per design)
- Backoff on 429 does not advance cursor twice incorrectly
These bugs live in client code and still present as "pagination is broken."
Reporting Pagination Defects Clearly
A weak bug report says "pagination broken." A strong one includes:
Endpoint: GET /orders
Params: page=2&perPage=10&status=paid&sort=createdAt:asc
Seed: 25 paid orders with ids O1..O25 in createdAt order
Page1 ids: O1..O10
Page2 ids: O9..O18 // duplicate O9 O10
Expected: O11..O20
Env: staging build 2026.07.09.3
Notes: default sort without id tie-breaker; two orders share timestamp
Attach the id lists. Developers fix ordering much faster with that evidence.
Load and Soak Considerations for List Endpoints
Functional correctness is necessary but not sufficient.
- Deep offset queries may degrade linearly. Capture p95 for
page=1vspage=1000in staging. - Cursor pagination usually stays flatter if indexed properly.
- Large
perPagevalues stress serialization and memory. - Concurrent walkers from analytics jobs can amplify DB load.
Add one soak scenario: continuously list with small page size for 30 minutes while writers insert. Watch error rates and memory, not only JSON shape.
Accessibility and Product UX Notes for API Testers
API testers still influence UX contracts:
- Totals help screen reader users understand result size when the UI exposes them.
- Stable ordering prevents "elements moved" confusion.
- Empty last pages should not break focus management in infinite lists.
When API metadata is incomplete, UI teams invent client-side guesses that create bugs. Push for clear hasMore and totals policies early.
Migrating From Offset to Cursor Without Breaking Clients
Many teams migrate as data grows. Test the migration window:
- Old clients still call
pageparams. - New clients call
cursor. - Both run against the same dataset.
- Feature flags flip percentages of traffic.
- Deprecation warnings appear in headers if promised.
Compatibility tests should freeze a consumer collection for each style until sunset dates pass.
Template: Pagination Test Case Pack You Can Copy
Use this pack as a starting checklist for every new list endpoint.
TC-PAGE-01 Default parameters return first page with documented size
TC-PAGE-02 limit/perPage minimum boundary (1)
TC-PAGE-03 limit/perPage maximum boundary
TC-PAGE-04 limit/perPage over maximum rejected or clamped per contract
TC-PAGE-05 Full walk uniqueness (no duplicate ids)
TC-PAGE-06 Full walk completeness (no missing ids on stable data)
TC-PAGE-07 Last page partial length
TC-PAGE-08 Beyond-last-page behavior
TC-PAGE-09 Invalid page/offset/cursor -> 400
TC-PAGE-10 Filter applied on every page
TC-PAGE-11 Sort stable with tie-breaker
TC-PAGE-12 Authz holds on page 2+
TC-PAGE-13 Metadata consistency (total/hasMore/next)
TC-PAGE-14 Concurrent write observational behavior documented
Copy these into your test management tool, then replace generic names with resource-specific titles. Automation can implement the walk invariants once and reuse them across resources. When a new list endpoint ships without this pack, treat the gap as incomplete API test design rather than a minor polish item.
Practice Path
In QABattle, pick an API battle with list endpoints and deliberately seed enough records to force multi-page walks. If you are new, sign up and practice writing the eight core cases from the page/limit table above until they are automatic.
Summary
Mastering how to test pagination in an API means verifying navigation metadata, page size bounds, full-collection integrity, filters, authorization, and behavior under change, not merely asserting status === 200 on a list call. Identify the pagination model, seed deterministic data, walk all pages, and lock boundaries into CI.
List endpoints are where products feel fast or broken. Make their pagination boringly correct.
FAQ
Questions testers ask
How do you test pagination in an API?
Seed enough data, request multiple pages with explicit page size, verify item counts, non-overlapping pages, stable sort order, totals or cursors, and empty-page behavior. Also test invalid page values, max limits, and consistency when data changes between requests.
What is the difference between offset and cursor pagination?
Offset (or page/limit) pagination jumps by numeric position and is easy to reason about but can skip or duplicate items when data changes. Cursor pagination uses an opaque pointer to the last seen item and is usually more stable for infinite scroll and large datasets.
What edge cases matter most for paginated APIs?
Zero results, last partial page, page size of 1, maximum page size, page beyond the end, negative or non-numeric inputs, default page size, duplicate prevention across pages, and sort stability when values tie.
Should totals be required in every pagination response?
Not always. Exact totals are useful for UI page counts but expensive on huge collections. Some APIs return hasMore or cursors only. Tests should match the contract: if total is promised, assert it; if not, assert navigation signals instead.
How many records do you need to test pagination well?
Enough to fill at least three pages at your chosen page size, plus a partial final page. For limit=10, seed 25-32 records. Also keep a zero-record dataset for empty-state checks.
Can pagination bugs cause security issues?
Yes. Broken filters combined with pagination can leak other tenants' rows on later pages. Over-large page sizes can enable scraping and performance abuse. Always combine pagination tests with authorization and rate-limit checks on list endpoints.
RELATED GUIDES
Continue the route
How to Write API Test Cases
How to write API test cases with practical templates, CRUD examples, auth checks, negative paths, and a review checklist for reliable service coverage.
API Testing Tutorial: A Beginner's Complete Guide
API testing tutorial for beginners: learn REST API checks, CRUD test cases, tools, status codes, and a practical checklist for reliable API quality.
REST API Status Codes: The Complete Reference for Testers
Master REST API status codes with a complete tester cheat sheet for 2xx, 4xx, and 5xx responses, error validation, and practical test design.
JSON Schema Validation in API Testing
Use JSON Schema validation in API testing to catch payload drift, required fields, types, and enum breaks with practical examples and a reusable checklist.