How to Fix Broken API Pagination

If ?page=2 returns the same rows as ?page=1, your OFFSET math is wrong. The correct formula is OFFSET = (page - 1) * limit. Here's the fix - and when to switch to cursor pagination.

Backend Engineernodejsexpresspostgresql

The bug: every page returns the same rows

Offset pagination slices results with SQL LIMIT (page size) and OFFSET (how many to skip). The classic bug is computing the offset as page * limit:

// BUG: page 1 -> OFFSET 10 (skips the first page!), and the math is off for every page
const offset = page * limit;

With page starting at 1, page * limit skips a whole page on page 1 and is wrong everywhere. If page is treated as 0-based elsewhere, you get the same rows repeated.

The fix: (page - 1) * limit

Pages are 1-based; the first page should skip nothing:

const limit = 10;
const offset = (page - 1) * limit;   // page 1 -> 0, page 2 -> 10, page 3 -> 20
const rows = await db.query(
  "SELECT * FROM products ORDER BY id LIMIT $1 OFFSET $2",
  [limit, offset]
);

Two things that matter: - Always ORDER BY a stable column - without it, OFFSET can return overlapping or missing rows because row order isn't guaranteed. - Return total count (SELECT count(*)) so clients can render page numbers.

Validate the edges

When to use cursor pagination instead

Offset pagination gets slow on big tables (the database still scans + discards all the skipped rows) and can skip/duplicate rows if data changes between pages. For large or fast-changing datasets, use a cursor: pass the last seen id and query WHERE id > $cursor ORDER BY id LIMIT $n. It's O(1) per page and stable under inserts.

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why does my API return the same results for every page?

The OFFSET is wrong - usually computed as page * limit instead of (page - 1) * limit, so it skips a page or repeats rows. Fix the formula and always ORDER BY a stable column.

What is the correct LIMIT/OFFSET formula for pagination?

LIMIT = page size, OFFSET = (page - 1) * page_size for 1-based pages. Page 1 -> offset 0, page 2 -> offset 10, and so on.

When should I use cursor pagination instead of offset?

On large or frequently-changing tables - offset gets slow (the DB scans and discards skipped rows) and can skip/duplicate rows mid-paging. A cursor (WHERE id > last_id) is fast and stable.

Keep learning

Return the Correct HTTP Status CodeBackend projectAdd CORS Middleware to an Express APIBackend projectAdd a Health Check Endpoint in GoBackend projectBackend roadmapStep by step to hiredBackend interview questionsSTAR answersAll Backend projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →