Pagination, Filtering, and Sorting

Common defects in list APIs and how to test pagination, filters, and sorting thoroughly.

Updated:

Contents

List APIs look simple, yet they contain many troublesome defects. Checking only that a list opens can leave duplicate or missing records, unstable ordering, incorrect total counts, broken filters, and inconsistencies between pages undiscovered.

Lists appear almost everywhere: users, orders, payments, logs, events, products, tasks, and comments. Poor pagination, filtering, or sorting makes a product seem chaotic: records disappear or appear twice, filters behave unexpectedly, ordering changes, and the next page presents inconsistent data.

Why APIs need these features

A server can return all records when there are few of them. As data grows, responses become heavy, rendering becomes difficult for the client, and the backend consumes unnecessary resources.

  • →Pagination returns data in portions
  • →Filtering returns the required subset
  • →Sorting defines item order

These mechanisms almost always work together. A user opens an order list, filters by status, sorts by date, and moves between pages. Instability in any one layer disrupts the entire experience.

Pagination

Pagination divides a large dataset into pages or batches.

  • →Offset-based pagination: page=2, limit=20 or offset=20&limit=20
  • →Cursor-based pagination: cursor=..., starting_after=..., next_page_token=...

QA needs to understand the difference.

Offset pagination is easy to read and common in administration interfaces and ordinary CRUD APIs. Its weakness is that page contents can shift when data changes between requests. A user sees 20 records on page one; someone adds a record; page two then contains duplicates or skips records.

Cursor pagination is generally more resilient with changing data. It suits large, frequently updated lists, event feeds, transactions, and high-load systems. Visual testing is harder because QA must track a cursor and item order instead of a familiar page number.

The main implication for QA is to test pagination with changing data as well as a static dataset.

Filtering

Filtering restricts a dataset according to conditions.

  • →status = active
  • →created_at >= date
  • →price between X and Y
  • →owner_id = current user
  • →Search by name or email
  • →Several filters applied together

This useful feature is also a frequent source of logic defects.

  • →A filter is partially ignored
  • →Two filters behave differently together than individually
  • →An empty filter and an omitted filter are treated differently
  • →Case sensitivity is unexpected
  • →Date filtering fails because of time zones
  • →Backend filtering differs from what the UI expects
  • →Permissions hide some records, leading a user to think the filter is broken

Testing filtering goes beyond checking a SQL condition or query parameter. It establishes whether the system returns precisely the subset its contract promises.

Sorting

Sorting defines result order, commonly by date, name, status, priority, price, or another field.

The most common mistake is a superficial check that the order looks approximately correct. In practice, sorting must be:

  • →Correct
  • →Stable
  • →Predictable
  • →Compatible with pagination

Stable order is especially important. Sorting only by a field with repeated values, such as identical created_at timestamps or status values, can leave the order of tied records inconsistent. With pagination, users may then see duplicate or missing records across pages.

QA should test ties as well as ordinary single-field ordering.

What matters for QA

Several checks are useful in almost every case.

For pagination:

  • →The first page
  • →The last page
  • →An empty page
  • →Page size = 1
  • →Page size at the maximum allowed value
  • →Page size above the allowed limit
  • →Moving between adjacent pages
  • →No duplicates or omissions across pages
  • →Correct has_more, next_cursor, and total_count values, where provided

For filtering:

  • →One filter
  • →Several filters together
  • →Boundary values
  • →An empty value
  • →An omitted filter
  • →An invalid filter value
  • →Filtering fields subject to access controls
  • →Date and time filtering

For sorting:

  • →Ascending and descending order
  • →Sorting by different fields
  • →Several records with equal values
  • →Sorting combined with filters
  • →Sorting combined with pagination
  • →An unsupported sort field

Common failure patterns

  • →An item appears on both the first and second pages
  • →An item disappears between pages
  • →The total_count differs from the actual count
  • →A filter applies to records but not the count
  • →The visible sort order changes between identical requests
  • →A new record shifts positions and disrupts offset-based navigation
  • →The backend silently ignores an unknown filter or sort option
  • →The UI expects one order while the API returns another
  • →Date filters produce different results because of time zones
  • →Filters and access controls interact unexpectedly

This is especially important when lists are the product's main working interface. Even minor instability can quickly undermine user trust.

Practical example

Consider an order list API:

  • →GET /orders?status=paid&sort=created_at_desc&limit=20

QA should check:

  • →Only paid orders are returned
  • →Orders are sorted by date in the requested direction
  • →Two orders with the same created_at retain a stable relative order
  • →The next page contains no duplicates or omissions
  • →The total_count, if present, corresponds to the filtered dataset
  • →Pagination behaves predictably if a new order appears during navigation
  • →Invalid sort or status values are handled clearly without breaking the service

A list can seem almost functional while these details make it unreliable in practice.

Key takeaways for QA

  • →Test pagination, filtering, and sorting together as well as individually.
  • →List APIs often fail at boundaries and combinations of conditions, beyond the happy path.
  • →Offset pagination is simple but sensitive to data changes between requests.
  • →Cursor pagination is more resilient with changing data but requires a different testing approach.
  • →Stable sorting is essential to correct pagination.
  • →Check total_count, has_more, cursors, and page boundaries as carefully as the records themselves.

Further reading

  • →In this knowledge base: Negative API Testing
  • →In this knowledge base: Methods, Status Codes, and Headers
  • →External resource: Stripe pagination

A 12-record dataset with known expected pages

The local fixture starts with IDs 1–12. Score is floor((id−1)/3), so three consecutive records share a score. Even IDs have status open and odd IDs closed. Sorting by score uses ID as a secondary key, which makes ties deterministic. Run the following checks against a fresh fixture without writes between page requests.

plain text
Query	Expected IDs	Total
?sort=score&limit=5&offset=0	1,2,3,4,5	12
?sort=score&limit=5&offset=5	6,7,8,9,10	12
?sort=score&limit=5&offset=10	11,12	12
?sort=score&limit=5&offset=12	(empty)	12
?status=open&sort=score&limit=5&offset=0	2,4,6,8,10	6
?status=open&sort=score&limit=5&offset=5	12	6

Flatten the first three pages and require exactly IDs 1–12, with no duplicates and no gaps. Check that total describes the filtered dataset before pagination, not the length of the current page. In this contract limit must be 1–5, offset is a nonnegative integer, and unsupported sort/filter values return 400.

Stable tie-breaking does not create a snapshot. If data changes between offset requests, records can move across boundaries. A separate experiment should define the consistency contract and insert/delete between requests; cursor-based pagination also needs a defined ordering and cursor validity policy. The supplied fixture exercises static offset behavior, not those additional guarantees.

Continue learning

Download the local practice lab (ZIP)

Inspect ordering and filtering in SQL

Reject invalid query parameters