# Consistent reads across pages Development builds support read-only snapshots on v3 instances. Use ordinary keyset pagination to browse the latest committed state at each request. Begin a snapshot when an export, report or scan must see the same documents in the same bytewise ID order on every page. Writers may commit between requests. Snapshot reads grant no write-transaction isolation or right to update an old version. ## Traverse a den On one authenticated Python connection, select the warren and den before beginning. An empty page can carry a continuation when a sparse equality filter reaches the examination budget. Only a null continuation means the traversal is complete. ```python client.select_warren("crm") client.select_den("contacts") view = client.begin_snapshot({"field": "team", "value": "blue"}) handle = view["snapshot"] cursor = None try: while True: page = client.snapshot_page(handle, 50, cursor) for document in page["documents"]: process(document) cursor = page["next_cursor"] if cursor is None: break finally: client.end_snapshot(handle) ``` Omit the filter to scan the entire den. Node exposes the same three asynchronous operations as `beginSnapshot(filter)`, `snapshotPage(handle, limit, cursor)` and `endSnapshot(handle)`. In the Rust network client, call `den.begin_snapshot(filter)?`, pass the returned SnapshotInfo by reference to `den.snapshot_page(&view, limit, cursor)?`, and pass each returned next_cursor to the next call. Release early with `den.end_snapshot(&view)?`. The shell prints the handle and cursor as JSON strings. Substitute their contents for HANDLE and CURSOR. ```text use crm/contacts snapshot begin team=blue snapshot page HANDLE 50 snapshot page HANDLE 50 CURSOR snapshot end HANDLE ``` Embedded Rust owns the continuation inside the snapshot object. Supply monotonically increasing milliseconds from the host each time. A page briefly borrows the owning database. ```rust let mut view = db.begin_snapshot("crm", "contacts", None, PageBudget::ordinary(50), now_ms())?; loop { let page = view.next_page(&mut db, now_ms(), 50)?; for document in page.documents { process(document); } if !page.has_more { break; } } view.close(); ``` ## Limits and lifecycle | Server flag | Default | What it bounds | |---|---:|---| | --snapshot-count | 16 | Active roots per open database. Set to zero to disable admission. | | --snapshot-ttl-ms | 60,000 | Absolute lifetime from begin, checked at page admission. | | --snapshot-retained-bytes | 268,435,456 | Sum of conservative bounds on all nodes reachable from each active root. | | --snapshot-page-size | 100 | Maximum documents requested per page. | | --snapshot-examined | 10,000 | Maximum candidate documents examined per page. | | --snapshot-response-bytes | 524,288 | Encoded page payload including room for structural fields and cursor. | A reader reserves for the whole database root, including its indexes, even when querying a small den. Shared nodes are charged separately for each reader. Admission fails with snapshot_limit when either count or bytes would exceed policy. Writers continue under the usual engine capacity limits. An oversized document returns a page error. Smaller limits can trade throughput for shorter individual requests. A successful request admitted before expiry can finish after its deadline. The final page, explicit end, disconnect, expiry or process exit releases the pin. In embedded code, dropping the handle also releases it. The server permits one active traversal per authenticated connection. Ending a completed traversal is acknowledged until the connection starts another one. A cursor holds an opaque handle and sequence. The server privately associates it with the generation, namespace, fixed equality filter and last scanned position. A lost page reply cannot be replayed. After a reconnect or restart, deliberately begin a new traversal if the application can accept a newer view. Wire commands are snapshot_begin with optional filter, snapshot_page with snapshot, limit and optional cursor, and snapshot_end with snapshot. Begin returns snapshot, generation, lifetime_ms and reserved_bytes. Page returns generation, documents, examined and next_cursor. Invalid, foreign and replayed continuations return snapshot_invalid. The most recently expired handle on that session returns snapshot_expired until a new begin. Authentication still uses the normal login. ## Storage and portability The captured v3 root resolves the catalog, postings and document bodies together, even if an index is edited or dropped. V3 already appends immutable nodes. Opening a read view adds no file records. Closing releases the reservation. To reclaim physical space, stop the server, run `ratdb migrate-indexed SOURCE NEW-FILE` and verify the new file. Migration creates a separate compacted image of current state. V1 files require migration for snapshot reads. Archived alpha binaries predate this interface. Custom adapters use the existing Storage trait and its exclusive ownership and durability contract. Core snapshot limits are configured with Database::set_snapshot_limits. The core needs an allocator and caller-provided time. Embedded hosts can reap idle leases with Database::snapshot_usage(now_ms). There are no snapshot-specific disk headers, background threads or network dependencies in the core.