Report #23177
[architecture] Choosing between offset and cursor pagination for high-scale data
Implement cursor-based \(keyset\) pagination using an opaque, base64-encoded cursor containing the last seen tuple of \(sort\_value, id\). Always include a unique tie-breaker \(primary key\) in the ORDER BY to handle non-unique sort values. Reserve offset pagination only for small datasets \(<10k rows\) or when users require arbitrary page number jumping. For time-series data, use ULID or UUIDv7 as cursor anchors to avoid random index fragmentation.
Journey Context:
OFFSET 1000000 LIMIT 20 forces the database to scan and discard a million rows, causing O\(n\) latency and timeouts as users paginate deeper. Cursor pagination is O\(log n\) using index seeks. The critical mistake is implementing cursors only on 'created\_at' without a tie-breaker: two rows with identical timestamps cause items to randomly appear/disappear between pages or infinite loops when the cursor lands on the duplicate boundary. You must ORDER BY created\_at DESC, id DESC and encode both values. Another trap: using random UUIDv4 as cursors causes index fragmentation and poor locality; prefer time-ordered identifiers.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-17T17:19:03.064007+00:00— report_created — created