Guarantees

Last updated 29 October 2026

View .md

Read this when you need to know what Tonbo guarantees for a database: when a commit is durable, and what happens when your program moves, runs twice or loses the network.

What is guaranteed

WhenWhat you can rely on
A statement returnsThe commit is on this machine. It survives the process crashing, not the loss of the machine or its disk.
synced() resolvesEvery commit made before the call is durable: two of three replicas, each in its own zone of the region, hold it, and older data moves to object storage in the same region. It survives the loss of the machine, of a replica or of a whole zone.
The program movesAny machine opens the database by name and finds every commit synced() confirmed.
Two instances runOnly the last one to open the database writes it. The commits of the other never reach Tonbo, and its next synced() says so.
The network dropsCommits continue on the machine and synced() waits, until 64 MiB of compressed commits wait; then writes fail and commit nothing.
AlwaysThe data stays in the region it was created in.
  YOUR MACHINE: a laptop, a server, a sandbox
  +--------------------------------------------------+
  |  your process                                    |
  |    node:sqlite --commit--> the local copy        |   t0  the statement returns,
  |                            on this disk          |       at local speed
  +-------------------------+------------------------+
                            |  in the background, over HTTPS
                            v
  TONBO, in the database's region: three replicas, one per zone
  +--------------+       +--------------+       +--------------+
  |    zone a    |<----->|    zone b    |<----->|    zone c    |
  +------+-------+       +------+-------+       +------+-------+
         |                      |                      |      t1  two of three hold it:
         +----------------------+----------------------+          synced() resolves
                                |  in the background
                                v
                      +-------------------+
                      |  object storage,  |                     t2  older snapshots and
                      |  the same region  |                         commits move here
                      +---------+---------+
                                |  opening by name where there is no copy:
                                v  the latest snapshot, then the commits after it
  ANOTHER MACHINE, any time later
  +--------------------------------------------------+
  |  your process --> the local copy, rebuilt. It is |
  |                   now the writer, and the one    |
  |                   before it is refused           |
  +--------------------------------------------------+

When a commit is durable

A database is a local copy on your machine and its data at Tonbo. A commit moves from one to the other in the background, and it is durable once it reaches Tonbo:

WhenWhere the commit isWhat it survives
The statement returnsThe local copy and a log on the same diskThe process crashing: the next open on this machine sends it. Not yet the loss of the machine or its disk
synced() resolvesTwo of three replicas, one per availability zone, and later object storage in the regionDurable: the loss of the machine, of a replica or of a whole zone

So await synced() before anything outside the process acts on a commit, such as a reply, an acknowledgement or a payment. Durability says where each region keeps the replicas and how maintenance pauses them. On your machine, the package’s SQLite extension logs each commit before the statement returns, and replication sends it to Tonbo.

A process killed with its disk, such as a sandbox that is thrown away, can lose every commit since its last resolved synced(). So where a step must survive the machine, await synced() after it, not only before the process exits:

db.prepare("INSERT INTO steps(body) VALUES (?)").run(result);
await db.synced(); // from here, losing the machine does not lose this step

When the program moves

Open the database by name on another machine. Where there is no local copy, the open downloads the latest snapshot of the database and the commits after it; where a copy is left, it only catches up on what it missed. The process that writes the database takes a snapshot whenever enough commits follow the last one, so the commits to replay stay few (see Cold starts for how long a rebuild takes, and Snapshots and S3 for how).

When two instances run

One process writes a database at a time: the last one to open it. A restarted agent that opens its database becomes its writer, even while the old instance is still running:

  instance A                 Tonbo                  instance B
  ----------                 -----                  ----------
  commit, synced() ------->  A writes the database
                             B writes the database <------ open("agent-42")
  commit (local only)
  synced()  <-- refused ---  A's commits are refused
  UrsulaResetError                                  carries on from every
  (kind fenced): stop                               commit A synced

The old instance’s later commits may still succeed on its own machine, but they never reach Tonbo: its next synced() throws UrsulaResetError with the kind fenced, and its statements fail once it learns that it no longer writes the database. This stops writes to the database, not the old instance’s calls to other services, so an agent that acts outside the process awaits synced() right before it acts: that call also checks that the instance still writes the database (see Run side effects once). Inside the writing process, use as many connections as you like (see Connections). The refusal is called fencing, and Ownership and fencing shows how it works.

When the network drops

Statements keep committing on the machine, and synced() waits until Tonbo takes the commits; neither throws for a network failure. db.status().replication says why commits wait. Once 64 MiB of compressed commits wait, writes fail with backlog_full and commit nothing, until Tonbo catches up (see The backlog).

Where the data lives

A database lives in one AWS region, fixed when it is created: its replicas run in three availability zones of that region, and its older snapshots and commits move to object storage in the same region. Every synced() waits for a round trip to the region, so create a database close to where its program runs (see Regions and URLs). To take a database elsewhere, or keep a copy to go back to after a mistake of your own, export it.

Each database is a durable stream of its commits, the open protocol from ElectricSQL, served by Ursula, Tonbo’s open-source stream server; Ursula in Tonbo describes the replicas and the quorum.