Rules: readers
Last updated 29 October 2026
Check these rules before you rely on a program that reads or tails a stream: each keeps it from missing or repeating messages.
The requests keep the stream’s URL in STREAM, a key in TONBO_API_KEY, offsets in NEXT and OFFSET, and the stream’s incarnation in INCARNATION, as Append, Read and tail and Snapshots and retention do.
Keep offsets exactly as the stream returns them
Offsets are opaque strings that sort in stream order. Resume from the last Stream-Next-Offset you saw, never from one you compute (see Read).
NEXT=<the Stream-Next-Offset value>
curl -sS -i "$STREAM?offset=$NEXT&live=long-poll&timeout_ms=30000" \
-H "Authorization: Bearer $TONBO_API_KEY"NEXT=$((LAST + 1)) # an offset is not a number to count onRead until the answer is up to date
One answer carries at most 8 MiB. An answer without Stream-Up-To-Date: true stopped short: read on from its Stream-Next-Offset.
let offset = "-1";
for (;;) {
const response = await fetch(`${stream}?offset=${offset}`, { headers });
handle(await response.text());
offset = response.headers.get("Stream-Next-Offset");
if (response.headers.get("Stream-Up-To-Date") === "true") break;
}const response = await fetch(`${stream}?offset=-1`, { headers });
handle(await response.text()); // at most 8 MiB: the rest is missingSave a reader’s offset with what it did
Tonbo keeps no state for a reader (see What readers see). A reader that acts on messages and saves its offset later reads them again after a crash in between; saved in one transaction, for example in a database, both happen or neither does.
db.exec("BEGIN");
for (const message of messages) apply(db, message);
db.prepare("UPDATE readers SET next = ? WHERE name = ?").run(next, "billing");
db.exec("COMMIT"); // the work and the offset, togetherfor (const message of messages) await act(message);
writeFileSync("next-offset", next); // a crash before this acts on them againReopen a tail from the last offset it gave you
The server sends no heartbeats, so a proxy may close a quiet tail. Open it again from the last streamNextOffset or Stream-Next-Offset; a tail opened at now skips what was appended while it was closed (see Tail).
curl -sS -N "$STREAM?offset=$NEXT&live=sse" \
-H "Authorization: Bearer $TONBO_API_KEY" \
-H "Accept: text/event-stream"curl -sS -N "$STREAM?offset=now&live=sse" \
-H "Authorization: Bearer $TONBO_API_KEY" \
-H "Accept: text/event-stream"Guard against a stream deleted and created again
A stream created again under the same name is a new stream with new offsets. Send its Stream-Incarnation, and a request to a replaced stream answers 412 instead of reading the new one (see A stream deleted and created again).
curl -sS -i "$STREAM?offset=-1" \
-H "Authorization: Bearer $TONBO_API_KEY" \
-H "Stream-Incarnation: $INCARNATION"Publish a snapshot, check it, then advance retention
Retention drops the history before the snapshot for good, and the two steps are separate so that you can read the snapshot back first (see Retention).
curl -sS -i -X PUT "$STREAM/snapshot/$OFFSET" \
-H "Authorization: Bearer $TONBO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"messages":3}'
curl -sS -i "$STREAM/snapshot/$OFFSET" \
-H "Authorization: Bearer $TONBO_API_KEY"
curl -sS -i -X PUT "$STREAM/retention/$OFFSET" \
-H "Authorization: Bearer $TONBO_API_KEY"