All guides

LEARN \u00b7 DEBUGGING GUIDE

Pagination off by one bug: how to debug pagination errors

Page 1 shows items 1-20. Page 2 should show items 21-40 but shows items 20-39 instead. The first item of page 2 is the last item of page 1. Or the last page is empty.

BeginnerDatabase/debugging

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.

( 01 )Fast diagnosis

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.
( 02 )Where to look

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?
( 03 )Common root causes

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)
( 04 )Fix patterns

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)
( 05 )How to verify

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.
( 06 )Mistakes to avoid

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