Common Pagination Bugs and How to Catch Them

Common Pagination Bugs and How to Catch Them

May 01, 2026 · 18 min read · Common Issues

Common Pagination Bugs and How to Catch Them

Pagination is a seemingly simple feature: split a large data set into bite‑size chunks and let users move forward or backward. In practice, the mechanics hide a surprising number of edge cases that slip past unit tests, pass manual sanity checks, and only reveal themselves under real‑world load or with specific user behaviors. This guide walks through the most frequent pagination defects, explains why they arise, shows how they appear to users, and gives concrete steps to reproduce, detect, fix, and prevent them.

---

Common Pagination Bugs and How to Catch Them: Overview

Before diving into specific patterns, it helps to view pagination as a contract between three parties: the data source (database, search index, or service), the transport layer (API endpoint or frontend component), and the presentation layer (list, infinite scroll, or page‑number control). A bug can violate any part of that contract.

Typical failure modes include:

Detecting these issues requires a mix of static analysis, targeted unit tests, and dynamic exploration that mimics real user journeys. The sections below break down the most common bug families, give reproducible examples, and show how to catch them with both manual and automated techniques.

---

Common Pagination Bugs and How to Catch Them in Offset‑Limit APIs

Offset‑limit pagination (often called “skip/take”) is the default for many SQL‑based APIs. Its simplicity masks several subtle pitfalls.

Why offset‑limit fails

When a client requests GET /items?offset=20&limit=10, the server executes something like SELECT * FROM items ORDER BY id LIMIT 10 OFFSET 20. If rows are inserted or deleted between requests, the logical slice shifts. A new row inserted before offset 20 pushes every existing row down by one, causing the item that was previously at position 20 to appear again on page 2. Conversely, a deletion removes a row and creates a gap that makes the client skip an item.

Symptoms to users

Reproduction steps

  1. Seed a table with 100 rows, ordered by an auto‑increment id.
  2. Request page 1 (offset=0, limit=10) and record the IDs returned.
  3. Insert a new row with id less than the current offset (e.g., id=5).
  4. Request page 2 (offset=10, limit=10).
  5. Compare the IDs; you will see the item that previously belonged to page 1 now appearing on page 2.

Detection techniques

Fixes

Prevention checklist

---

Common Pagination Bugs and How to Catch Them in Cursor‑Based APIs

Cursor‑based pagination solves the offset problem but introduces its own failure modes, especially around cursor encoding, expiration, and ordering ties.

Why cursor pagination can break

A cursor is typically a base64‑encoded string that contains the value(s) of the ordering column(s) for the last item returned. If the encoding is mishandled (e.g., URL‑unsafe characters not escaped), the client may send a malformed cursor that the server treats as null, returning the first page again.

If the ordering columns are not unique, ties cause nondeterministic ordering: two rows with the same timestamp may appear in either order across pages, leading to duplicates or omissions.

Symptoms to users

Reproduction steps

  1. Create two records with identical created_at timestamps (e.g., both at 2025-01-01T12:00:00Z).
  2. Request the first page (limit=5). The server returns records A, B, C, D, E (where A and B share the timestamp).
  3. Encode the cursor using the timestamp of E and a tie‑breaker (e.g., id). If the implementation only uses the timestamp, the cursor is ambiguous.
  4. Request the next page with that cursor; you may receive A and B again, or you may skip them entirely.

Detection techniques

Fixes

Prevention checklist

---

Common Pagination Bugs and How to Catch Them in Infinite Scroll UIs

Infinite scroll replaces explicit page controls with a sentinel that triggers a fetch when the user nears the bottom of the list. The UI layer introduces timing, state, and rendering bugs that are invisible in API‑only tests.

Why infinite scroll fails

Symptoms to users

Reproduction steps (manual)

  1. Load a list that uses infinite scroll with a page size of 20.
  2. Scroll slowly until the trigger fires; note the network request.
  3. While the request is still pending, continue scrolling quickly to the bottom.
  4. Observe whether a second request is sent before the first resolves.
  5. After the first response renders, check if any item from the first batch appears again in the second batch.

Automated detection

Fixes

Prevention checklist

---

Common Pagination Bugs and How to Catch Them in Page‑Number Controls

Traditional page‑number widgets (e.g., “1 2 3 … 10 [Next]”) are common in admin dashboards and search results. Bugs here often revolve around stale page counts, incorrect handling of the last page, and accessibility oversights.

Why page‑number controls fail

Symptoms to users

Reproduction steps

  1. Load a list with 95 items, page size = 20 (so 5 pages).
  2. Navigate to page 4 (items 61‑80).
  3. Delete 10 items from the range 81‑95 (now total = 85, 5 pages still, but last page holds only 5 items).
  4. Click “Next”. Observe whether the UI shows page 5 correctly or stays on page 4.
  5. Remove another 20 items (total = 65, now only 4 pages). Click “Next” again; the UI should either hide the button or show a message that there are no more pages.

Detection techniques

Fixes

Prevention checklist

---

Common Pagination Bugs and How to Catch Them in Search Result Pages

Search paginates results based on relevance scores, filters, and faceted navigation. The interaction between ranking, faceting, and pagination creates bugs that are hard to anticipate with simple keyword tests.

Why search pagination fails

Symptoms to users

Reproduction steps

  1. Index a collection of products with fields: title, category, brand, price.
  2. Execute a broad query ("camera") with sort=score_desc and page_size=10. Record the first 20 results (pages 1‑2).
  3. Apply a facet filter brand:Canon.
  4. Request page 1 again; compare the IDs of the first ten results with the first ten from step 2. Expect no overlap if the facet is restrictive.
  5. If overlap appears, note the duplicated IDs.
  6. Also request the facet counts before and after applying the filter; verify they match the actual hit counts.

Detection techniques

Fixes

Prevention checklist

---

Using Persona‑Driven Autonomous Exploration to Surface Pagination Bugs

Scripted tests excel at checking known paths, but they often miss the combinations of user behavior, data mutation, and timing that expose pagination flaws. Autonomous QA platforms—like SUSATest—can simulate a variety of user personas, each with distinct interaction patterns, and thereby uncover bugs that remain hidden in conventional test suites.

How personas affect pagination

PersonaTypical behaviorPagination‑specific risk
CuriousTaps every item, scrolls quickly, opens filtersMay trigger rapid successive page requests, exposing race conditions
ImpatientScrolls to bottom instantly, clicks “Load more” repeatedlyCan cause overlapping requests or exhaust server rate limits
NoviceRelies on visible page numbers, rarely uses infinite scrollMay notice off‑by‑one errors in page‑number controls
AdversarialEnters malformed cursors, attempts SQL injection via pagination paramsCan reveal insufficient input validation or cursor‑ parsing bugs
Elderly / AccessibilityUses keyboard navigation, screen reader, high contrastMay uncover missing ARIA labels, focus traps, or insufficient touch targets
Power userApplies multiple facets, sorts, and paginates deeplyCan expose stale total counts, facet‑count mismatches, or deep‑page performance issues
Data‑mutatingInserts, deletes, or updates records while browsingHighlights offset‑limit drift, keyset expiration, or snapshot inconsistencies

When an autonomous agent explores an app with these profiles, it dynamically varies:

The agent records every network call, UI state change, and console error, then evaluates heuristics such as: duplicate item detection, missing items after a known insert, or sudden jumps in scroll position.

Concrete example with SUSATest

Suppose we have an e‑commerce infinite‑scroll product list. We configure a persona “Impatient” with a scroll burst of 500 px every 200 ms and a “Data‑mutating” persona that inserts a new product every 3 seconds via a background API.

The SUSATest agent runs the following pseudo‑flow:


// SUSATest persona script (simplified)
await page.goto('https://shop.example.com/products');
let lastY = 0;
while (!page.isFinished()) {
  // Impatient scroll burst
  await page.evaluate(() => window.scrollBy(0, 500));
  await page.waitForTimeout(200);
  // Data‑mutating action (runs in parallel)
  await page.request.post('/admin/products', { 
    data: { name: 'TestItem', price: 9.99 } 
  });
  // Check for duplicates
  const ids = await page.$$eval('.product-card', els => els.map(e => e.dataset.id));
  if (new Set(ids).size !== ids.length) {
    page.reportBug('Duplicate product IDs detected during infinite scroll');
  }
}

If the backend uses offset‑limit pagination, the injected items will cause the visible product IDs to shift, and the duplicate check will fire. The same script run against a keyset‑based endpoint would pass, demonstrating how autonomous exploration can validate the effectiveness of a fix.

Benefits over scripted tests

Practical tips for integrating autonomous exploration

  1. Seed realistic personas – start with the six profiles above and adjust parameters (scroll speed, mutation rate) to match your product’s usage analytics.
  2. Instrument duplicate detection – embed a lightweight hash of visible item IDs into the window object; the test harness can read it after each interaction.
  3. Fail fast on performance degradation – monitor response times for pagination requests; a sudden increase beyond a threshold (e.g., 2× baseline) flags a potential offset‑limit scan issue.
  4. Correlate with logs – send the agent’s session ID to your backend logs; you can then trace which server‑side query produced duplicate or missing rows.

---

Practical Test Matrix for Pagination Validation

Below is a matrix that maps common pagination scenarios to manual validation steps, automated checks, and the tools best suited for each. Use this as a starting point when building a test suite for a new endpoint or UI component.

ScenarioManual validation stepsAutomated check (example)Tools / Frameworks
Offset‑limit API with concurrent inserts1. Load page 1, note IDs.
2. Insert a row with ID < offset.
3. Load page 2, verify no duplicate/missing IDs.
`js
test('offset limit duplicate detection', async () => {
const page1 = await api.get('/items?offset=0&limit=20');
await db.insert({id:5, ...});
const page2 = await api.get('/items?offset=20&limit=20');
const ids = [...page1.data.map(i=>i.id), ...page2.data.map(i=>i.id)];
expect(new Set(ids).size).toBe(ids.length);
});`
Jest / Mocha + Supertest, any SQL DB fixture
Cursor‑based API with tie‑breaking1. Create two rows with same timestamp, different IDs.
2. Fetch first page, capture cursor.
3. Fetch second page using cursor; ensure no overlap.
`js
test('cursor tie breaker', async () => {
await db.createMany([{ts:now, id:1},{ts:now, id:2}]);
const p1 = await api.get('/items?limit=1&sort=ts');
const cursor = encodeCursor(p1.data[0].ts, p1.data[0].id);
const p2 = await api.get(/items?limit=1&sort=ts&cursor=${cursor});
expect(p2.data[0].id).toBe(2);
});`
Playwright API test, Node
Infinite scroll list (UI)1. Scroll to trigger load.
2. While loading, scroll further quickly.
3. Verify no duplicate item IDs appear.
`js
test('infinite scroll no duplicates', async () => {
await page.goto('/feed');
let seen = new Set();
await page.evaluate(async () => {
let resolve;
const wait = new Promise(r=>resolve=r);
const observer = new IntersectionObserver((entries)=>{
if(entries[0].isIntersecting){ resolve(); }
},{threshold:0.1});
const sentinel = document.createElement('div');
document.body.appendChild(sentinel);
observer.observe(sentinel);
await wait;
});
const ids = await page.$$eval('.item', els=>els.map(e=>e.dataset.id));
expect(new Set(ids).size).toBe(ids.length);
});`
Playwright + IntersectionObserver polyfill
Page‑number control with total updates1. Navigate to middle page.
2. Delete enough items to shrink total pages.
3. Click “Next”; verify correct page or disabled button.
`js
test('pagination clamps after delete', async () => {
await page.goto('/admin/users');
await page.selectOption('#pageSize','20');
await page.click('text=Page 3');
await db.deleteMany({id:{$gt:80}}); // reduce total
await page.click('text=Next');
const disabled = await page.isDisabled('button[aria-label="Next"]');
expect(disabled).toBe(true);
});`
Jest + Puppeteer, API for DB mutation
Search results with facet & pagination1. Perform broad query, capture first 20 IDs.
2. Apply a facet, capture first 20 IDs after facet.
3. Assert low overlap.
4. Validate facet count matches hit count.
`js
test('search facet pagination consistency', async () => {
const r1 = await searchApi.get('/search?q=camera&limit=20');
const facet = {brand:'Canon'};
const r2 = await searchApi.get(/search?q=camera&limit=20&fbrand=${encodeURIComponent(facet.brand)});
const overlap = r1.data.filter(i=>r2.data.some(j=>j.id===i.id)).length;
expect(overlap).toBeLessThan(3);
const facetCount = await searchApi.get(/search/facets?q=camera&fbrand=${encodeURIComponent(facet.brand)});
expect(facetCount.data[0].count).toBe(r2.data.length);
});`
Jest + fetch, Elasticsearch/OpenSearch mock
Keyset API expiration after update1. Fetch first page, capture last sort value.
2. Update the sorted column of the last item on the page.<>3. Request next page using old cursor; expect 410 Gone or empty.
`js
test('keyset expiration', async () => {
const p1 = await api.get('/items?limit=10&sort=updated_at');
const last = p1.data[p1.data.length-1];
await db.update(last.id, {updated_at: new Date()});
const cursor = encodeCursor(last.updated_at, last.id);
const p2 = await api.get(/items?limit=10&sort=updated_at&cursor=${cursor});
expect(p2.status).toBe(410); // or empty array
});`
AVA + supertest

The matrix makes it clear that a combination of manual spot checks (good for exploratory testing) and automated assertions (essential for CI) yields the strongest safety net.

---

Fix Strategies and Prevention Checklist

Having examined the bugs, their origins, and detection methods, we now consolidate remediation into a concise, actionable checklist that can be woven into your Definition of Done (DoD) or sprint‑planning routine.

1. Choose the right pagination model early

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free