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

# Snapshots and S3

Last updated 29 October 2026

Read this when you need to know what Tonbo keeps of a database, and where: how the process that writes it keeps its stream short enough to rebuild from, and what Ursula keeps in S3.

## When the owner publishes a snapshot

Ursula [stores snapshots but does not make them](https://ursula.tonbo.io/docs/concepts/snapshots/): the process that owns the database does. A snapshot [is due](https://github.com/tonbo-io/ursula/blob/v0.11.2/docs/architecture/sqlite-vfs.md#snapshots) once the commits acknowledged since the latest one take more than the database’s size or 8 MiB, whichever is larger. A thread of the owning process then:

1. Checkpoints the WAL and copies the database as of the last logged commit.
2. Waits until the stream acknowledges that commit, at an offset W.
3. Compresses the copy with [Zstandard](https://www.rfc-editor.org/rfc/rfc8878.html) behind a 56-byte header that names W, and publishes it at W, keyed by W and the body’s digest. A body larger than one request goes up in parts.
4. Moves the stream’s retention to the snapshot before this one, so the history it keeps readable is always one snapshot behind.

```text
  offset 0                                                    tail
  |-- older log --|== snapshot at W1 ==|-- log --|== snapshot at W2 ==|-- log --|
                   retention: readable                  latest:  a cold open reads
                   history starts here                  this snapshot, then the
                   (one snapshot behind)                log after W2
```

A publish that fails waits from 100 ms, doubling, up to five minutes before the next try. `db.status().snapshot` shows the log written since the latest snapshot (`log_bytes`), the threshold (`due_bytes`) and the last error. Tonbo supports databases whose snapshot is up to 256 MiB compressed (see [Limits](https://tonbo.dev/docs/databases/limits.md)).

## A cold open

A host without a copy of the database builds one from the latest snapshot and the log after it, in a directory it renames into place only once the copy is complete, and then claims the database:

```text
  open("notes") on a host with no copy
        |
        |  HEAD                     -> the latest snapshot's offset, W
        |  GET snapshot/W, by range -> gen-<id>.tmp/db
        |  GET the stream from W    -> apply the commits after it
        |  rename gen-<id>.tmp      -> gen-<id>, the copy is ready
        v
  claim: this process owns the database
```

Every read goes to Ursula, which [streams the snapshot’s body](https://ursula.tonbo.io/docs/concepts/snapshots/#size-and-content-type) and the older log from S3 when they are there. Your host never reads S3. [Cold starts](https://tonbo.dev/docs/databases/cold-starts.md) says how long a build takes.

## What Ursula keeps in S3

A write [lives first in memory and in the Raft logs](https://ursula.tonbo.io/docs/architecture/overview/#storage-layout) of the voters. Ursula’s cold tier then moves to S3, in the background:

| What | When |
| --- | --- |
| A stream’s committed log, in chunks | A [flush runs every second](https://github.com/tonbo-io/ursula/blob/v0.11.0/docs/architecture/bounded-stream-state.md#f10-flush-planner). It moves a group’s committed bytes once the group holds 8 MiB of them, a stream’s whole tail once it is 5 minutes old, and small backlogs once the groups a voter leads hold 128 MiB together. Small chunks are merged later |
| A snapshot’s body | On publish, for a body of 1 MiB or more. A replaced body is deleted 5 minutes after the snapshot that replaced it |
| Ursula’s own snapshots of each Raft group | As each group’s Raft log grows, compressed with Zstandard and written with a [conditional write](https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-writes.html), so no snapshot replaces another |

After an upload succeeds, Ursula records the object’s reference through Raft. A read then takes recent bytes from memory and older ones with [ranged reads](https://docs.aws.amazon.com/AmazonS3/latest/userguide/range-get-olap.html) of S3, and any voter that applied the reference can serve it. Ursula writes S3 through [Apache OpenDAL](https://docs.rs/opendal/0.51.2/opendal/).

Compression happens before data reaches Ursula, and once inside it. The extension compresses every commit’s pages and every snapshot with Zstandard on your host (see [The SQLite extension](https://tonbo.dev/docs/databases/internals/sqlite-extension.md)), and Ursula stores the stream’s chunks in S3 as it received them, without compressing them again. Ursula compresses its own Raft group snapshots with Zstandard before it writes them to S3, and the [Raft traffic between voters](https://github.com/tonbo-io/ursula/blob/v0.11.0/docs/architecture/thread-per-core-multiraft.md#raft-transport) too: streamed requests always, other requests from 1 KiB.

## Tonbo’s S3 settings

| Setting | Value |
| --- | --- |
| Where | One private store per region, in the database’s region |
| [Public access](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html) | Blocked |
| [Encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html) | SSE-S3 (AES-256) on every object |
| [Versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html) | On |
| [Lifecycle](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html) | Replaced and deleted versions expire after 30 days; incomplete multipart uploads are aborted after 7 |
| [Storage class](https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html) | S3 Standard; no rule moves objects to another class |

## What S3 does not give you yet

S3 holds a database’s flushed log and a snapshot body of 1 MiB or more, but not a database on its own: the newest writes are only on the voters until they are flushed, and Ursula cannot yet rebuild a deployment from S3 alone: [its roadmap](https://github.com/tonbo-io/ursula/blob/v0.11.0/README.md#roadmap) lists recovery from the cold tier alone as not done, and today a total loss needs a backup made with [Ursula’s backup tool](https://ursula.tonbo.io/docs/operations/#backup-and-disaster-recovery), which Tonbo does not run for databases. An export is what takes a database out of Tonbo (see [Backups and salvage](https://tonbo.dev/docs/databases/backups.md)).
