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

# Replication and synced()

Last updated 29 October 2026

Read this when you need to know how commits leave your machine, beyond what [When a commit is durable](https://tonbo.dev/docs/databases/how-it-works.md#commit) promises: how the extension sends them to the database’s stream, how it retries, and what `synced()` waits for.

## One append at a time

Each database has [one replicator](https://github.com/tonbo-io/ursula/blob/v0.11.2/docs/architecture/sqlite-vfs.md#replicator) in its process. It reads the commits the [extension](https://tonbo.dev/docs/databases/internals/sqlite-extension.md) logged and sends them, in order, as appends to the stream, with at most one append in flight:

```text
  your host                               Tonbo, in the database's region
  ---------                               -------------------------------
  log  [c1][c2][c3][c4][c5]
        acknowledged | pending
                     |
  replicator --- POST c3..c5 (one batch) ---> gateway ---> the group's leader
                                                            |  Raft: 2 of 3
                                                            |  voters hold it
  c3..c5 acknowledged <--- 200, Stream-Offset -------------+
  (one append in flight per database)
```

When no append is in flight, it [seals the pending records, oldest first, into one batch](https://github.com/tonbo-io/ursula/blob/v0.11.2/docs/architecture/sqlite-vfs.md#appends) of at most 4 MiB, or less when the stream advertises a smaller request limit. A commit larger than that goes out as a run of parts, one append each, which readers apply only once the run is complete. An append is an HTTP request of the [Durable Streams Protocol](https://github.com/durable-streams/durable-streams/blob/main/PROTOCOL.md) with Ursula’s producer headers:

```text
POST <database URL>
Content-Type: application/vnd.ursula.sqlite-vfs.v2
Stream-Incarnation: <the stream's history token>
Producer-Id: sqlite-ursula-vfs/2
Producer-Epoch: <this owner's epoch>
Producer-Seq: <the next sequence number>
Producer-Digest: <BLAKE3-256 of the body>

<one batch of compressed commit records>
```

The stream acknowledges an append once two of the three voters hold it in their Raft logs and it is applied (see [Ursula in Tonbo](https://tonbo.dev/docs/databases/internals/ursula.md)). The answer names the offset where the batch starts, which must continue exactly where the previous acknowledged write ended.

## Retries without duplicates

An append without a definite answer is sent again, byte for byte, with no retry budget, however long an outage lasts. The wait between attempts [doubles from 25 ms to 5 s with full jitter](https://github.com/tonbo-io/ursula/blob/v0.11.2/docs/architecture/sqlite-vfs.md#appends), honours a `Retry-After` of up to 60 s, and is 30 s while a condition blocks replication.

A repeat [cannot apply twice](https://ursula.tonbo.io/docs/concepts/exactly-once-writes/). The stream keeps, for each producer, its epoch, its highest sequence number and the receipt of its newest write, and every append carries the producer id, the epoch, the sequence number and a digest of its body. A repeat of a write that already applied is [answered `204` with the original receipt](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers). So [a proxy or the gateway sending an append again](https://ursula.tonbo.io/docs/examples/sqlite-vfs/#layers-in-front-of-the-stream) does no harm.

| Answer to an append | The replicator |
| --- | --- |
| `200`, or `204` (a repeat), at the expected offset | Marks the batch acknowledged |
| No answer, a timeout, a `5xx`, `429` or `408` | Sends the same request again after the backoff |
| `401` | Rereads the token and sends again; the same token refused twice blocks as `unauthorized` |
| `402`, or `403` from the gateway | Blocks as `payment_required` or `forbidden`, and keeps trying |
| `403` naming a newer epoch | Stops: another process owns the database (see [Ownership and fencing](https://tonbo.dev/docs/databases/internals/ownership.md)) |
| A redirect | Never follows it; after three, blocks as `redirected` |

## What synced() waits for

`synced()` takes as its target the last commit logged when it is called. It [resolves once both of these hold](https://github.com/tonbo-io/ursula/blob/v0.11.2/docs/architecture/sqlite-vfs.md#synced):

1. The stream acknowledged every commit up to the target: two of three voters hold them.
2. A round trip to the stream that started after the call found nothing written after this process’s own commits. That proves the process still owns the database.

```text
  synced() called           target = the last logged commit, c5
        |
        |  writing:  the append that carries c5 is answered at the
        |            expected offset ----------------------------> resolves
        |
        |  idle:     a leader read at the stream's tail, made after
        |            the call, returns no bytes -------------------> resolves
        |
        |  another owner took the database ---------> rejects (fenced)
        |  the signal fires -------------------------> rejects (signal.reason)
```

While the process writes, its appends are the round trip: an answer at the expected offset shows nobody else wrote in between. An idle owner pays one read at the stream’s tail instead, made with [`consistency=leader`](https://ursula.tonbo.io/docs/specs/extensions/#92-leader-consistency): the group’s leader first confirms with a majority of voters that it still leads, so the read sees every write acknowledged before it. Concurrent calls share one round trip. `synced()` never throws for a network failure: it waits, and rejects only on a reset or when its [signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) fires, while the commits keep replicating.

## The backlog

Commits that are logged and not yet acknowledged are pending, and `db.status().commits.pending_bytes` counts their compressed size. Once it reaches [`maxPendingBytes`](https://ursula.tonbo.io/docs/examples/sqlite-vfs/#open-options) (64 MiB by default), the next write transaction [fails at its first write](https://github.com/tonbo-io/ursula/blob/v0.11.2/docs/architecture/sqlite-vfs.md#backpressure) with `backlog_full`, having written nothing; reads go on, and the database stays usable. This is the only point where the stream’s state stops a commit: a refused token, a `402` or a `403` blocks replication, not commits, until the backlog fills. The bound also caps what a reset can discard.

## Replication conditions

`db.status().replication.condition` is `null` while appends succeed, and otherwise says why they are being sent again:

| Condition | Meaning |
| --- | --- |
| `retrying` | No definite answer yet: the network, the gateway or a leader change |
| `unauthorized` | The token was refused twice |
| `forbidden` | The token may not write this database |
| `payment_required` | The account cannot pay for writes (see [When the balance runs out](https://tonbo.dev/docs/databases/pricing.md#zero-balance)) |
| `request_too_large` | A layer in front of the stream allows less than 4 KiB |
| `redirected` | Something answered with redirects |
| `wrong_cluster`, `no_server_identity`, `protocol_mismatch` | The URL leads to another deployment, to something that is not Ursula, or to another release line |
| `rejected` | Any other answer the stream cannot take |

Commits continue under every condition until the backlog fills. [Troubleshooting](https://tonbo.dev/docs/databases/troubleshooting.md) says what to do about each.
