Skip to content

no-offset-pagination

Potentially unbounded OFFSET pagination in store SQL.

Why

Dynamic offsets may require scanning discarded rows and may skip or repeat results during concurrent writes.

Fix

Use a stable, immutable, unique ordering and a cursor predicate with the same direction; include a unique tie-breaker for non-unique sort columns. Keep OFFSET with a reasoned suppression for demonstrably bounded or static data, or an intentional random-access page contract.

Examples

Before — flagged Page selected with an offset
app/task_store.py
QUERY = "SELECT id FROM task ORDER BY id LIMIT :limit OFFSET :offset"
After — preferred Page selected with a stable composite cursor
app/task_store.py
QUERY = "SELECT id FROM task WHERE (created_at, id) < (:cursor_created_at, :cursor_id) ORDER BY created_at DESC, id DESC LIMIT :limit"