Cursor pagination, not OFFSET, past the first few pages
OFFSET 100000 reads and throws away a hundred thousand rows to return ten. And a row inserted between two page loads 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 holds still when new rows arrive.
httpapi-designpostgresql