DEV Community

Young Gao
Young Gao

Posted on

Pagination Done Right: Cursor vs Offset and Why It Matters at Scale

Pagination Done Right: Cursor vs Offset and Why It Matters at Scale

Your API uses OFFSET for pagination. At page 10000, queries take 30 seconds. Here is why, and the fix.

The Problem with OFFSET

-- Page 1: fast (skip 0, read 20)
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 0;

-- Page 500: slow (database reads and discards 10000 rows)
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 10000;
Enter fullscreen mode Exit fullscreen mode

OFFSET scans and discards rows. The deeper you paginate, the slower it gets.

Cursor-Based Pagination

app.get("/api/posts", async (req, res) => {
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);
  const cursor = req.query.cursor; // Last item ID from previous page

  let query = db("posts").orderBy("created_at", "desc").limit(limit + 1);

  if (cursor) {
    const cursorPost = await db("posts").where({ id: cursor }).first();
    query = query.where("created_at", "<", cursorPost.created_at);
  }

  const posts = await query;
  const hasMore = posts.length > limit;
  if (hasMore) posts.pop();

  res.json({
    data: posts,
    pagination: {
      hasMore,
      nextCursor: hasMore ? posts[posts.length - 1].id : null,
    },
  });
});
Enter fullscreen mode Exit fullscreen mode

When to Use Which

  • Cursor: Infinite scroll, real-time feeds, large datasets. O(1) regardless of page depth.
  • Offset: Admin dashboards, small datasets, when users need to jump to specific pages.
  • Keyset: Similar to cursor but uses composite keys for complex sorting.

Part of my Production Backend Patterns series. Follow for more practical backend engineering.

Top comments (0)