Give a changing list a stable reading point
An API cursor should say what ordering and visibility it preserves while new records arrive
An operator opens a list of recent dispatches and reads the first page. Before they ask for the next page, a new dispatch arrives. If the API uses a numeric offset, that arrival can move an earlier item onto the next page. The operator sees it twice. If an item disappears, they may miss another record entirely.
This is not a cosmetic pagination bug. A list is often used to reconcile work, investigate a delay, or hand a shift to another person. The API should say what it means to continue reading while the underlying set changes. A stable cursor can preserve an ordering boundary, but only if the contract makes its limits explicit.
Choose the order before choosing the cursor
A cursor cannot fix an undefined order. Recent first sounds clear until two records share the same timestamp. The server needs a deterministic tie breaker, such as a stable identifier, so every record has a place in the sequence. The cursor can then carry the last seen position in that total order.
Suppose the list sorts by creation time descending, then by identifier descending. The next page asks for records strictly after the last item in that order. Newer arrivals stay above the reading point and do not shift the next page. The operator can finish the older part of the list without an item being repeated simply because another one arrived.
The response should document the order in language a caller can use. If the API later changes the sort rule, old cursors may no longer mean the same thing. Version the cursor or reject it clearly. A silent reinterpretation can turn a working reconciliation loop into a source of missed records.
Be honest about what remains fluid
A stable ordering boundary does not necessarily give a frozen snapshot. A record already seen might change status. A record not yet seen might be removed or become invisible under permissions. A new record might be inserted with an older event time. The caller needs to know whether the cursor promises a snapshot, a traversal of currently visible records, or only a continuation after one position.
For many operational lists, a continuation boundary is enough. The caller can finish walking older records, then refresh from the top for new work. If the task requires an exact historical set, a snapshot token or export job may be needed. Do not imply snapshot semantics just because the cursor is opaque.
Filtering makes the boundary more delicate. A cursor created for failed dispatches should not be reused for completed dispatches. If the filter changes, the next page should reject the cursor or start a new traversal. Bind the cursor to the relevant query shape and access scope, so one user's position cannot be repurposed to reveal another user's list.
Make an expired cursor recoverable
Cursors should not be permanent capabilities. An API may expire them after a reasonable period or after its underlying snapshot is gone. When that happens, return a distinct error with a safe recovery instruction. An expired cursor is different from an empty page. Treating both as no results can make a client think it completed the traversal.
The recovery path depends on the reader's task. A dashboard can restart from the newest page. A batch reconciliation job may need to restart from a known timestamp and stable identifier, then deduplicate by record ID. Document the option the API actually supports. Never advise a caller to skip ahead by guessing a numeric offset.
A cursor should not leak sensitive list state through its raw form. Sign or otherwise validate it if clients can tamper with it. Keep authorisation checks on every page request. An opaque token is a convenience for carrying a position, not proof that the bearer may read the records behind it.
Test with writes between page requests
A pagination test that seeds a fixed list and reads every page misses the failure that matters. Fetch page one, insert records above and below the boundary, update a status used by a filter, then request page two. Check for repeats and omissions under the documented contract. Repeat with identical timestamps to prove the tie breaker works.
Test expiry separately. A client should be able to distinguish an expired cursor from a finished list and choose the documented recovery path. Also test a cursor used with a different filter or account. The server should reject that mismatch rather than returning a plausible but unrelated page.
Stack Dispatch exposes work that can keep moving while someone reads it. We should give callers a stable reading point and a truthful account of what stability means. The cursor should let a reader continue through a defined order, while the API makes fresh arrivals, changed records, and expired positions explicit enough to handle safely.
0 comments