# Time travel

Source: https://stratadb.org/docs/learn/time-travel

Strata keeps what each version held, so reading the past is a read rather than a
restore. There is no snapshot to mount and no backup to unpack: you name a point
and run an ordinary command against it.

This page assumes [Commits and versions](/docs/learn/commits-and-versions),
which is where versions, timestamps and history come from.

## Reading at a point

Add `--as-of` to a read and it answers as of that position on the timeline:

```console
strata kv get k --as-of 4
```

The rule is **the latest committed state at or before the point you named**. That
"or before" is the whole behaviour, and it is easy to miss until you watch it.
Take a key written at timestamp 3 and again at 5, with somebody else's write
landing at 4:

| `--as-of` | answer  | why                                                    |
| --------- | ------- | ------------------------------------------------------ |
| 2         | nothing | the key did not exist yet                              |
| 3         | `v1`    | the write landed here                                  |
| 4         | `v1`    | nothing touched this key at 4, so 3 is still the truth |
| 5         | `v2`    | the second write                                       |
| 6         | error   | past the end of the timeline                           |

You do not have to hit a point where your record changed. Any point resolves
to what a reader would have seen standing there.

Deletions are respected the same way. A key deleted at timestamp 7 reads as
present at 6 and absent at 8, because the tombstone is part of the history
rather than an absence in it.

## Which reads take a point

Temporal reading is not uniform across the five data models. Before you build on
it, know what you have.

| model     | reads that take `--as-of`                                                                             |
| --------- | ----------------------------------------------------------------------------------------------------- |
| Key-value | `get`, `list`                                                                                         |
| JSON      | `get`, `list`                                                                                         |
| Events    | `get`, `count`, `list`, `types`, `by-type`                                                            |
| Vectors   | `get`, `query`                                                                                        |
| Graph     | `meta`, `get-node`, `list-nodes`, `get-edge`, `neighbors`, `nodes-by-type`, `bfs`, and every analytic |
| Branches  | `diff`                                                                                                |

Three patterns are worth pulling out of that table.

**`scan` never takes a point, and `list` always does.** That holds for key-value,
JSON and vectors alike. If you need a full-row historical read, `list` the keys
as of your point and `get` each one at the same point.

**Graph goes deepest.** Not just the reads but the whole analytics suite, so you
can run a community detection or a PageRank against the graph as it stood
earlier and compare. That is a genuinely different question from "what does the
graph look like now", and it is one command:

```console
strata graph wcc g --as-of 7
```

```
component_count: 2
```

against a graph that today reports one component, because the edge that joined
them landed after that point.

**Events are the exception, and for a reason.** `event range` and
`event range-time` do not take `--as-of`, because the log already carries its own
clock. Each entry records when it occurred, so asking for a window of the log is
already a question about time. What you cannot do is ask what the log looked like
at an earlier commit, which is a different question and one the log's own
ordering makes largely moot.

## Enumerating the past takes care

A paginated read is several commands, and the database can move between them.
JSON states this explicitly: each page reads the latest committed state unless
you pin it, so documents created behind your cursor never appear and ones ahead
of it show up in later pages.

For a stable enumeration, pass the same `--as-of` on every page:

```console
strata json list --limit 2 --as-of 42
strata json list --limit 2 --as-of 42 --cursor k:2
```

Without that, a long enumeration over a changing database is not a snapshot of
anything. With it, every page answers as of the same instant.

## Two clocks, and which flag reads each

History gives you three ways to name a moment, and they are not
interchangeable:

```json
{ "version": 38, "timestamp": 88, "committed_at": "2026-09-07 00:09:21.577135 -05:00" }
```

`--as-of` takes the **timestamp**: a position on the logical commit timeline, a
per-commit counter that has nothing to do with the calendar. Passing the
`version` instead does not raise an error, and in a fresh database the two
numbers happen to be equal so the mistake works. In a database that came from a
clone they differ and it quietly returns nothing.

`--as-of-time` takes a real moment, and accepts most ways of writing one:

```console
strata kv get k --as-of-time 2026-09-05
strata kv get k --as-of-time "2026-09-05 15:00"
strata kv get k --as-of-time 2026-09-05T15:00:00Z
```

A time without an offset is read as local time. The two flags cannot be
combined; doing so fails with `invalid_argument.executor.as_of_conflict`.

Use the logical clock when you need exact reproducibility, and the wall clock
when you have a real moment in mind. `committed_at` is best effort: it comes
from the machine that made the commit, so a clock adjustment can move it
backwards. Only the logical timeline is guaranteed ordered.

### Wall-clock reads stop at the last commit

Asking for a moment later than the newest dated commit is an error rather than a
synonym for "now":

```console
strata kv get k --as-of-time "2026-09-07 09:00"
```

```
history_unavailable.engine.persistence_history:
  wall-clock instant is after the latest dated commit
```

That is deliberate. A point-in-time read that silently answered from the present
whenever the instant was in the future would be indistinguishable from one that
found what you asked for.

Two more limits are worth knowing before you rely on the wall clock. Commits
made before this feature existed carry no instant at all, so a wall-clock query
landing before the first dated commit refuses with its own reason. And commits
produced by replay or import are undated by design, because sampling a clock
during deterministic replay would make it non-deterministic.

## Branching from the past

A point-in-time read answers a question. A branch from that point gives you
somewhere to work.

```console
strata branch fork default before --version 3
```

`before` now holds the database as it stood at version 3, and it is an ordinary
branch: you can write to it, compare it against the branch it came from, and
merge it. The parent keeps moving without disturbing it.

> **Figure.** A branch taken from an earlier version starts from what the database held then, and stays isolated as the parent keeps committing past that point.

This is the recovery story, and it is worth seeing as one. A bad migration does
not need a restore: branch from the version before it, check the state is what
you expected, and merge that back. Nothing was rolled back, nothing was lost,
and the bad versions stay in the history where you can still read them.

## History is retained, not permanent

Time travel reaches as far back as the retained history and no further. Ask for
a point beyond the timeline and Strata says so with
`history_unavailable.engine.persistence_history` rather than answering from the
nearest thing it has.

That is the right failure. A point-in-time read that silently substitutes a
different point is worse than one that refuses, particularly for the audit and
recovery work this feature exists for.