> Fetch the index of every Tonbo docs page as Markdown at https://tonbo.dev/llms.txt

# Read and tail

Last updated 29 October 2026

Read a stream from any offset, catch up to its end, and wait there for what is appended next with a long-poll or Server-Sent Events.

Any number of readers read one stream, each from its own offset, and reads are free. The examples use `curl` with the stream’s URL in `STREAM` and an API key in `TONBO_API_KEY`, as the [quickstart](https://tonbo.dev/docs/getting-started/quickstarts/stream.md#key) sets them; a public stream needs no key (see [Public streams](https://tonbo.dev/docs/streams/access.md#public)).

## Read

`GET` with an `offset` reads from that offset to the end:

```shell
curl -sS -i "$STREAM?offset=-1" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

| Offset | Reads from |
| --- | --- |
| `-1` | The start of the stream. |
| A `Stream-Next-Offset` | Where an earlier read or append ended: what came after it. |
| `now` | The current end, for a tail that wants only new messages. |

A read answers `200` with what it found and `Stream-Next-Offset`, the offset to read from next; with `Stream-Up-To-Date: true` it reached the end. One answer carries at most 8 MiB: an answer without `Stream-Up-To-Date` stopped short, so read again from its `Stream-Next-Offset`. Offsets are opaque strings that sort in stream order; keep them as they are (see [offsets](https://ursula.tonbo.io/docs/concepts/offsets/)). A read normally comes from the replica that answers; add `consistency=leader` to see every append acknowledged before it (see [leader consistency](https://ursula.tonbo.io/docs/specs/extensions/#92-leader-consistency)). The [read reference](https://ursula.tonbo.io/docs/api/read/) has every parameter and answer of a read.

## Tail

A read at the end with `live` waits for the next append. A long-poll answers once it has something:

```shell
curl -sS -i "$STREAM?offset=$NEXT&live=long-poll&timeout_ms=30000" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

It answers `200` with what was appended, or `204` when `timeout_ms` passes first (1,000 by default, at most 60,000); read again from its `Stream-Next-Offset`. A Server-Sent Events read stays open instead:

```shell
curl -sS -N "$STREAM?offset=$NEXT&live=sse" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Accept: text/event-stream"
```

It sends an `event: data` with what was appended, then an `event: control` whose JSON names `streamNextOffset`. A data event’s lines are transport framing, not message boundaries: a JSON client joins them and splits the messages at each newline. A binary stream’s data comes base64-encoded (see [binary SSE](https://ursula.tonbo.io/docs/concepts/binary-sse/)). The server sends no heartbeats, so a proxy may close a quiet connection: open it again from the last `streamNextOffset`. The [SSE event format](https://ursula.tonbo.io/docs/api/read/#sse-event-format) has the details. Each open tail counts toward the account’s live reads in the region (see [Limits](https://tonbo.dev/docs/streams/limits.md)).

## To a web page

A page should never hold an API key. Either your server mints the page a read-only token for only the streams it shows, which the page tails directly (see [A token for a web page](https://tonbo.dev/docs/streams/access.md#browser)), or your server holds one tail and passes each message on to the pages that watch, reopening the tail from its last offset when it drops, and catching up a page that reconnects with a read from the last message it showed. The second keeps one open tail however many pages watch; [Live progress to a web UI](https://tonbo.dev/docs/streams/job-progress.md#browser) walks through it. A public stream needs no token, so a page reads it directly.

## A closed stream

Once its writer has [closed](https://tonbo.dev/docs/streams/append.md#close) a stream, a reader at the end learns it at once: a read answers `200` with nothing and `Stream-Closed: true`, a long-poll `204` with `Stream-Closed: true` without waiting, and an SSE read a control event with `streamClosed: true`, after which the server ends the connection; a [HEAD](#head) answers `Stream-Closed: true` too. Everything before the end stays readable.

## HEAD

`HEAD` reads a stream’s state without its contents:

```shell
curl -sS -I "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

It answers `200`, or `404` for a stream that does not exist, and it sees every append acknowledged before it was sent. Its headers, of which the [HEAD reference](https://ursula.tonbo.io/docs/api/head-stream/) lists a few more:

| Header | Meaning |
| --- | --- |
| `Content-Type` | The stream’s content type, which every append sends. |
| `Stream-Next-Offset` | The end of the stream: where the next append starts. |
| `Stream-Closed` | `true` once the stream is closed; absent before. |
| `Stream-Retained-Offset` | The earliest offset still readable; reads before it answer `410`. It is the start of the stream until [retention](https://tonbo.dev/docs/streams/snapshots.md#retention) advances. |
| `Stream-Snapshot-Offset`, `Stream-Snapshot-Digest` | The offset and digest of the latest [snapshot](https://tonbo.dev/docs/streams/snapshots.md#publish); absent when there is none. |
| `Stream-Incarnation` | Which stream answered: it changes when the stream is deleted and created again (see [below](#incarnation)). |
| `Ursula-Max-Request-Bytes` | The largest request body the stream accepts. |

## A stream deleted and created again

A stream deleted and then created again under the same name is a new stream at the same URL, with new offsets. Each answer carries `Stream-Incarnation`, which changes when that happens, and `tonbo stream show` prints it as `Incarnation`. Send it back as a request header, and the request applies only to that stream: once the stream has been replaced it answers `412` and changes nothing (see [guarding against a recreated stream](https://ursula.tonbo.io/docs/concepts/conditional-writes/#guarding-against-a-recreated-stream-stream-incarnation)).

## Reads you can cache

A stream’s history before its end, and its latest snapshot, never change. Two reads name them by URLs that stay valid for as long as the stream does, with the stream’s `Stream-Incarnation` in the path, kept in `INCARNATION`:

```shell
curl -sS -i "$STREAM/incarnation/$INCARNATION/chunk/$OFFSET" \
  -H "Authorization: Bearer $TONBO_API_KEY"

curl -sS -i "$STREAM/incarnation/$INCARNATION/snapshot/$OFFSET" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

The first reads the stream from `OFFSET` to the next 4 MiB boundary, or to its end, with `Stream-Next-Offset` for the next chunk; the second reads the latest snapshot at its offset. An answer that can never change carries `Stream-Immutable: true`; a chunk that holds the open end of the stream does not, so read on from there with an ordinary read. Use them when your program keeps its own cache of what it read: every reader of a stream asks for the same URLs, and an answer with the marker can be kept for good. An answer to a request with a key or token is never marked for a shared cache, so only your program keeps it. A stream deleted and created again has another incarnation, so an old URL answers `412` instead of returning the old stream’s bytes (see [immutable reads](https://ursula.tonbo.io/docs/api/immutable-reads/)).

## When a read is refused

The table says, for each refusal a read can get, whether to send it again; a read changes nothing, so sending it again is always safe. [What readers see](https://tonbo.dev/docs/streams/guarantees.md#readers) says what a read promises, and [Troubleshooting](https://tonbo.dev/docs/streams/troubleshooting.md) lists every refusal.

| Answer | When | Send again | What to do |
| --- | --- | --- | --- |
| `401` | The key or token is missing, expired or revoked, or the request sent a key that acts as an Agent, which answers the code `exchange_required`. | With a valid key or a new token | Send a valid key, or exchange the key for a new token, as `Authorization: Bearer`. An Agent’s key is always exchanged first (see [A short-lived token](https://tonbo.dev/docs/streams/access.md#token)). |
| `403` | The key is narrowed and does not reach this stream, or is read-only and the request appends. | No | Use a key that reaches the stream, or one that is not read-only (see [Narrow a key to streams](https://tonbo.dev/docs/streams/access.md#narrow)). |
| `404` | The stream does not exist yet, is still being created, or was deleted or expired; a request with a missing or expired token may answer the same. | Once it is active | Check it with `tonbo stream show <name>`, create it first, and wait until it is `active`. |
| `410` | The offset is before the stream’s retention, which a snapshot replaced. | From the answer’s offset | Read the snapshot, or read from the answer’s `Stream-Next-Offset`, the earliest offset still readable. |
| `412` | The request carried Stream-Incarnation, and the stream was deleted and created again since. | No | Treat it as a new stream: read it from the start, with the answer’s Stream-Incarnation. |
| `429` | The account sent more requests per second, or held more tails open at once, than its region allows. | After Retry-After | Wait as long as `Retry-After` says, then send it again. |
| `503` | The region cannot take the request, or check an API key sent directly, for a moment. | Unchanged, after Retry-After | Wait as long as `Retry-After` says, then send the same request again; with producer headers, a repeated append applies once. |
