> ## Documentation Index
> Fetch the complete documentation index at: https://s2.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Snapshots

> Use snapshots to bound replay cost while continuing to follow newer stream records.

## Snapshot and follow

Many applications store state as an ordered stream of deltas: each record says what changed. A reader can recover current state by folding those records from the head, then keep the read open to [follow new records](/concepts/reads).

However, that replay cost grows with the stream. The snapshot-and-follow pattern bounds it by materializing state at a known stream position, or a *cursor*. The cursor is the first sequence number not covered by the snapshot — it can come from a writer's current state, or a separate snapshotting process can `checkTail` to ensure its knowledge is current.

## Where snapshots live

### External snapshots

For snapshots stored outside the stream, such as in object storage, attach the cursor to the snapshot itself. For example, include it in the object key or metadata.

To restore, load the snapshot, then read from the cursor and follow the stream. [Trimming](/concepts/trimming) the stream to the cursor is optional; do it when you want to discard history already covered by the snapshot.

For example, a snapshot at cursor `1000` covers records before `1000`, so recovery resumes at `1000`:

```mermaid theme={null}
flowchart LR
  subgraph Stream["stream sequence"]
    direction LR
    R0["0"] --> RMore["..."] --> R999["999"] --> R1000["1000"] --> R1001["1001"] --> RNew["new records"]
  end

  R999 -. "fold into" .-> Snapshot["snapshot<br/>covers 0..999"]
  R1000 --- Cursor["cursor = 1000<br/>first record not covered"]
  Snapshot --> Restore["restore<br/>load snapshot"]
  Restore --> R1000
  R1000 --> Follow["read and follow"]

  classDef covered fill:#e0f2fe,stroke:#0284c7,color:#0f172a;
  classDef cursor fill:#fef3c7,stroke:#d97706,color:#0f172a;
  classDef follow fill:#dcfce7,stroke:#16a34a,color:#0f172a;
  class R0,RMore,R999,Snapshot covered;
  class R1000,Cursor,Restore cursor;
  class R1001,RNew,Follow follow;
```

1. A snapshotting process folds sequence numbers `0` through `999` into state.
2. It writes `snapshots/orders/1000.json`, where `1000` is the cursor.
3. It optionally trims the stream to `1000` to discard the covered records.
4. A reader loads that object, reads from sequence number `1000`, and follows from there.

### In-stream snapshots

Storing the snapshot in the stream lets readers recover from the stream alone. When paired with [trimming](/concepts/trimming), the snapshot record or first fragment can become the new head, so readers simply start from the first record of the stream, apply the snapshot, then follow later deltas.

Because the snapshot is written to the stream, make the first append [conditional](/concepts/concurrency-control#match-sequence-number) on the cursor. If the stream has moved, rebuild or extend the snapshot and retry.

#### Single-record snapshot

If the snapshot and trim command will fit in a [1 MiB batch](/platform/limits#records), use a single atomic batch to append the [`trim` command](/concepts/command-records) and snapshot record. The command that advances the head and the snapshot that remains there become durable together.

1. Choose cursor `1000`.
2. Build or serialize the snapshot for that position.
3. Append one batch with `match_seq_num = 1000`:
   * a `trim` command record to `1001`
   * a snapshot record, e.g. `{ "type": "snapshot", "covers_before": 1000, "state": ... }`
4. On success, the trim command lands at sequence number `1000` and the snapshot lands at `1001`. After trimming takes effect, the command record is removed and the snapshot is the stream head.
5. Readers start at the head, apply the snapshot, and follow later deltas.

#### Framed snapshot

For snapshots too large for one record, split the snapshot into ordered fragments. The fragment records should be distinguishable from normal deltas, usually with headers or body fields that identify a snapshot ID, chunk order, and final marker. Every fragment is written before trimming.

1. Choose cursor `1000`.
2. Build or serialize the snapshot for that position.
3. Append the first snapshot fragment with `match_seq_num = 1000`.
4. Append the remaining fragments and a final marker in order, for example by using an append session or by waiting for each append before sending the next one.
5. Append a `trim` command record to `1000`.

After trimming takes effect, the first snapshot fragment is the stream head. Unlike the single-record case, the trim command remains in the retained stream. Readers can skip it when applying application deltas.

While a reader reconstructs the snapshot from its fragments, it may encounter delta records appended concurrently with the snapshot fragments. The reader should buffer those deltas, finish applying the snapshot, then apply the buffered deltas in order and continue following the stream.

## See also

<CardGroup cols={3}>
  <Card title="Trimming" icon="scissors" href="/concepts/trimming">
    Discard records already covered by a snapshot.
  </Card>

  <Card title="Reads" icon="list-timeline" href="/concepts/reads">
    Read from a cursor and follow new records.
  </Card>

  <Card title="Concurrency control" icon="sliders" href="/concepts/concurrency-control">
    Use conditional appends with match sequence numbers.
  </Card>
</CardGroup>
