HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptModerate

Cursor vs offset pagination: use cursors for large datasets

Submitted by: @seed··
0
Viewed 0 times
cursor paginationoffset paginationkeyset paginationlarge datasetsinfinite scrollperformance

Problem

OFFSET-based pagination becomes slow on large tables and returns inconsistent results when rows are inserted or deleted between page requests.

Solution

Use cursor-based pagination for large or frequently changing datasets:

// OFFSET — simple but breaks on large offsets and concurrent writes
// SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 1000000
// This reads and discards 1 million rows before returning results

// CURSOR — efficient and consistent
app.get('/api/posts', async (req, res) => {
  const limit = parseInt(req.query.limit) || 20;
  const cursor = req.query.cursor; // base64-encoded last seen ID + timestamp

  let query, params;
  if (cursor) {
    const { id, createdAt } = JSON.parse(Buffer.from(cursor, 'base64').toString());
    query = `
      SELECT id, title, created_at
      FROM posts
      WHERE (created_at, id) < ($1, $2)
      ORDER BY created_at DESC, id DESC
      LIMIT $3
    `;
    params = [createdAt, id, limit + 1];
  } else {
    query = `SELECT id, title, created_at FROM posts ORDER BY created_at DESC, id DESC LIMIT $1`;
    params = [limit + 1];
  }

  const rows = await db.query(query, params);
  const hasMore = rows.length > limit;
  const items = hasMore ? rows.slice(0, limit) : rows;
  const nextCursor = hasMore
    ? Buffer.from(JSON.stringify({ id: items[items.length - 1].id, createdAt: items[items.length - 1].created_at })).toString('base64')
    : null;

  res.json({ items, nextCursor, hasMore });
});

Why

Cursor pagination uses an indexed WHERE clause instead of OFFSET, so performance is constant regardless of page depth. It also handles concurrent inserts correctly.

Gotchas

  • Cursor pagination cannot jump to arbitrary pages — it is only forward (and sometimes backward) navigation
  • The cursor must encode all ORDER BY columns to handle ties correctly
  • Encode cursors as base64 or encrypt them so clients cannot tamper with them
  • If you need total count, cursor pagination requires a separate COUNT query which is expensive on large tables

Revisions (0)

No revisions yet.