What this usually means
Pagination math has three numbers: page number, offset, and limit. If any of them is off by one, the boundary between pages shifts. The most common bug: using page 0 internally but page 1 in the UI, or vice versa. Offset = (page - 1) * limit works for 1-indexed pages. Offset = page * limit works for 0-indexed pages. Using the wrong formula moves every page boundary by one item.
The first ten minutes \u2014 establish facts before touching code.
- 1Check the offset calculation. For 1-indexed pages: offset = (page - 1) * limit. For 0-indexed pages: offset = page * limit.
- 2Check what page number the API expects. Does it start at 0 or 1? Does the frontend send 0 or 1?
- 3Check if the total count includes all items or only the current filter. An incorrect total makes the last page calculation wrong.
- 4Check the LIMIT/OFFSET values in the actual database query. Enable query logging to see the exact SQL.
- 5Test the boundary: page 1, page 2, and the last page. Compare results for overlap or gaps.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchAPI route handler — how page number and limit are parsed from the request
- searchDatabase query — the exact LIMIT and OFFSET values sent to the database
- searchFrontend pagination component — how it calculates and sends the page number
- searchTotal count query — does it match the filtered results or all results?
- searchAPI response — does it include total, page, limit, and hasMore metadata?
Practical causes, not theory. These are the things you will actually find.
- warningOffset calculated as page * limit instead of (page - 1) * limit for 1-indexed pages
- warningFrontend sends 0-indexed pages but the API expects 1-indexed (or vice versa)
- warningTotal count includes filtered-out items, inflating the page count
- warningLIMIT is applied before OFFSET, or OFFSET before LIMIT, depending on the database
- warningLast page has fewer items than the limit, causing an empty final page if handled incorrectly
- warningCursor-based pagination uses the wrong comparison (>= instead of >, or vice versa)
Concrete fix directions. Pick the one that matches your root cause.
- buildStandardise on 1-indexed pages in the API and document it clearly
- buildUse offset = (page - 1) * limit consistently across all queries
- buildValidate input: reject page < 1 and limit < 1
- buildReturn hasMore or nextCursor in the response so clients know when to stop
- buildWrite a helper function for pagination that takes page and limit and returns offset, used everywhere
- buildTest the edge case where total items % limit === 0 (the last page is exactly full)
A fix you cannot prove is a guess. Close the loop.
- verifiedRequest page 1: should return items 1 through limit, and total should be correct.
- verifiedRequest page 2: first item should be limit + 1, not duplicate page 1's last item.
- verifiedRequest the last page: all items should be present, no items duplicated from previous page.
- verifiedRequest page beyond the last: should return empty list or 404, not an error.
- verifiedChange the page size and verify pagination still works correctly.
Things that make this bug worse or harder to find.
- warningUsing 0-indexed pages without documenting it
- warningNot validating page and limit parameters
- warningNot testing the last page boundary
- warningCalculating total pages without considering the remainder: Math.ceil(total / limit)
- warningBuilding pagination from scratch instead of using a well-tested library or database feature