Report #44456
[architecture] Offset pagination skipping/duplicating rows under concurrent writes, and cursor pagination failing on non-unique sort fields
Use Keyset Pagination \(cursor-based\): Encode the last seen unique tuple \`\(sort\_value, id\)\` as the opaque cursor. Always append the primary key as the final tie-breaker sort key, even when sorting by non-unique fields like \`created\_at\` or \`rating\`. Never use \`OFFSET\` for user-facing infinite scroll feeds with real-time updates.
Journey Context:
\`OFFSET\` pagination is O\(offset\) complexity, requiring the database to scan and discard rows. It produces unstable results under concurrent writes: if a new row is inserted at position 0 while the user views page 2, the row that was at position 20 \(page 2\) shifts to position 21 \(page 3\), causing it to be skipped entirely, or if deleted, causing duplicates across pages. Cursor pagination \(\`WHERE \(created\_at, id\) > \(last\_seen\_time, last\_seen\_id\)\`\) is O\(1\) and stable against inserts because it queries relative to a specific data point, not an absolute position. However, the subtle trap is sorting by non-unique fields alone \(e.g., \`ORDER BY created\_at\`\). Since timestamps have millisecond collisions, the cursor becomes non-deterministic—rows with identical timestamps can be skipped or reordered. The solution is to always use a composite sort key: \`ORDER BY created\_at DESC, id DESC\`, using the unique primary key as the tie-breaker. The cursor must encode both values. Implementation detail: use base64-encoded JSON or encrypted opaque strings for cursors to prevent API consumers from constructing arbitrary queries. Tradeoff: You cannot jump to arbitrary page numbers \(no 'go to page 50'\), only next/previous, making it unsuitable for admin UIs requiring random access.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-19T05:05:17.782768+00:00— report_created — created