Back to guides

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.

By The Testing AcademyPublished July 9, 2026Updated July 9, 202616 min read

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

ModelTypical paramsResponse hintsCommon UI
Offset / limitoffset, limittotal, itemsAdmin tables
Page / per_pagepage, per_pagetotalPages, pageClassic pagination controls
Cursorcursor, limitnextCursor, hasMoreInfinite scroll feeds
Keysetafter_id, limitnext_after_idLarge ordered tables
Link headeroften cursor/offsetLink: rel=nextGitHub-style APIs
GraphQL connectionsfirst, afterpageInfo, edgesModern 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:

  1. Deterministic ordering when sort is specified (and documented default sort when not).
  2. No duplicates across a full walk of the collection for a stable dataset.
  3. No missing items across a full walk for a stable dataset.
  4. Page size honored up to documented maximums.
  5. Final page behavior is clear (partial list, empty list, or error).
  6. Invalid inputs fail predictably.
  7. Filters apply across all pages, not only page 1.
  8. 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:

DatasetSizePurpose
empty0empty state
single1single-item page
exact30exact multiple of limit 10
partial25final partial page
large500+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

InputExpected (example policy)
limit=11 item when data exists
limit=maxmax items
limit=max+1400 or clamped to max (document which)
limit=0400 or empty data with 200 (document which)
limit=-1400
limit=abc400
limit omitteddefault

Never assume clamp vs error. Observe, confirm, then lock.

Step 7: Boundary test page positions

For page model:

  • page=1 first page
  • last page exact
  • last page partial
  • page=totalPages+1 empty data or 404 per contract
  • page=0 or negative -> 400

For offset model:

  • offset=0
  • offset=total-1
  • offset=total empty
  • 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:

  1. Create multiple rows with identical sort key if possible.
  2. Paginate with small limit.
  3. 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:

  1. Fetch page 1.
  2. Insert a new record at the top of default sort.
  3. 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

IDTitleStepsExpected
PG-001Default listGET /orders200, 20 items, page=1, total=25, totalPages=2
PG-002Page 2 partialperPage=20&page=25 items, no overlap with page 1
PG-003Full walk perPage=10pages 1-310+10+5 unique ids = all 25
PG-004perPage maxperPage=10025 items, totalPages=1
PG-005perPage too largeperPage=101400 with validation error
PG-006page beyond endpage=5&perPage=10200 with data=[] OR documented error
PG-007filter paid onlystatus=paid across pagesonly paid, totals match filtered count
PG-008invalid pagepage=-1400

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

IDTitleExpected
CR-001First pagehasMore true when more exist, nextCursor non-null
CR-002Walk until endhasMore false, nextCursor null/absent, full unique set
CR-003Reuse exhausted cursorempty data or same end behavior, no 500
CR-004Tampered cursor400
CR-005limit=1 walkN requests for N items
CR-006Concurrent insert during walkdocument 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

AssertionPage/offsetCursor/keyset
Exact total countOften yesSometimes no
totalPages mathYesNo
nextCursor present when hasMoreNoYes
Random access to page 5YesUsually no
Stability under insertsWeakerStronger typically
Easy UI "jump to page"YesNo

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=100000 may 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.hasNextPage and endCursor consistency.
  • Confirm edges.cursor advances.
  • Test first/last and forward/backward pagination if both exist.
  • Ensure nodes length respects first.

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

  1. Seed 40 orders across two statuses and mixed created times.
  2. Filter status=paid (expect 22 paid).
  3. Sort createdAt asc, id asc.
  4. Walk with perPage=5.
  5. Assert concatenated ids equal the filtered sorted baseline.
  6. Assert total equals 22 if totals are offered.
  7. Change sort to createdAt desc and 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.

PromiseMeaningTest approach
Snapshot isolationPages reflect collection at first requestHard to get without server support; rare
Continuous consistencyEach page reads latest dataExpect possible skips/dupes under writes
Keyset stabilityWalk by key does not replay earlier keysCursor tests under inserts
Best effortNo strong promiseDocument 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=1 vs page=1000 in staging.
  • Cursor pagination usually stays flatter if indexed properly.
  • Large perPage values 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:

  1. Old clients still call page params.
  2. New clients call cursor.
  3. Both run against the same dataset.
  4. Feature flags flip percentages of traffic.
  5. 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.