Skip to main content
REST API

Cursor vs Offset on a Mutating Dataset

RequestValidateMedium

What you will learn

  • Demonstrate how offset pagination skips/duplicates rows when the dataset mutates mid-scan
  • Use opaque keyset cursors for stable iteration and treat a malformed cursor as a 400
  • Choose cursor over offset whenever 'paginate through everything' must be exhaustive and correct

Description

See how offset paging skips/duplicates rows when the dataset changes between pages, and how keyset cursors stay stable.

Test Scenario

// Offset page 2 after inserts overlaps page 1; the opaque cursor does not.
const p1 = await (await request.get('/api/v1/feed?offset=0&limit=10')).json();
const p2 = await (await request.get('/api/v1/feed?offset=10&limit=10&_insert=5')).json();
const overlap = p1.data.filter((a) => p2.data.some((b) => b.id === a.id));
expect(overlap.length).toBeGreaterThan(0); // duplicates โ€” offset is unsafe under inserts

const bad = await request.get('/api/v1/feed?cursor=not-a-cursor');
expect(bad.status()).toBe(400);
Endpoint/api/v1/feed

Testing Tips

  • โ€ขThe ?_insert=N knob simulates rows added between page fetches (newest-first feed)
  • โ€ขOffset paging shifts after inserts โ†’ the same item appears on two pages (or one is skipped)
  • โ€ขKeyset cursor paging (?cursor=) reads id < lastSeen, so inserts above the cursor never disturb it
// Playwright API Testing - REST
import { test, expect } from '@playwright/test';
test('REST API request', async ({ request }) => {
const response = await request.get('/api/v1/resource');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const data = await response.json();
expect(data).toBeDefined();
});

Challenge ID: rest-pagination-drift

2 frameworks available

API Playground

Send requests and inspect responses

Endpoint: /api/v1/feed

Press Enter to send