1/6

offset=3&limit=3 means "count three rows from the start of the collection as it exists right now". That is a promise you cannot keep across more than one request.

Delete one row between page 1 and page 2 and every row after it shifts down one. Page 2 starts one row late, and a row the client has never seen is skipped forever — no error, no gap in the output, nothing to alert on. An insert produces the mirror bug: a duplicate.

A cursor is an opaque token encoding the last row's position in a stable total order:

SELECT * FROM rows WHERE id > $cursor ORDER BY id LIMIT 3

The next page is defined by content, not by a count, so edits before the cursor cannot shift it. Two contract details: order by something uniquecreated_at alone loses rows sharing a timestamp, so the key is (created_at, id) — and keep the token opaque (base64 the tuple) so you can change what is inside it without breaking clients.

Termination is part of the contract: next_cursor is absent on the last page. That, not an empty page, is how a client knows it is done.

The cost argument for cursors — that OFFSET is O(offset + limit) and a keyset seek is O(log n + limit) at any depth — is the subject of The Data Layer, lesson 4. This lesson is about the other half: what the two contracts promise a client whose collection is changing underneath it.