Rules: readers

Last updated 29 October 2026

View .md

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).

Do
NEXT=<the Stream-Next-Offset value>
curl -sS -i "$STREAM?offset=$NEXT&live=long-poll&timeout_ms=30000" \
  -H "Authorization: Bearer $TONBO_API_KEY"
Don’t
NEXT=$((LAST + 1)) # an offset is not a number to count on

Read 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.

Do
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;
}
Don’t
const response = await fetch(`${stream}?offset=-1`, { headers });
handle(await response.text()); // at most 8 MiB: the rest is missing

Save 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.

Do
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, together
Don’t
for (const message of messages) await act(message);
writeFileSync("next-offset", next); // a crash before this acts on them again

Reopen 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).

Do
curl -sS -N "$STREAM?offset=$NEXT&live=sse" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Accept: text/event-stream"
Don’t
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).

Do
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).

Do
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"