Cursor pagination, not OFFSET, past the first few pages
OFFSET 100000 reads and discards a hundred thousand rows to return ten, and a row inserted between two pages shifts everything so the client sees a duplicate or a gap.
SELECT * FROM events WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC LIMIT 50;The cursor is the last row's (created_at, id), encoded. Every page costs the same and the list is stable under inserts.
httpapi-designpostgresql