Snapshot Cursors
This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at README.md, and llms.txt lists every page.What it shows
A snapshot cursor gives strict snapshot isolation.
OpenSnapshotEntryCursorAsync freezes the tree state at open time; every page
the cursor returns reflects that captured instant, and no concurrent change -
foreground SetAsync / DeleteAsync, atomic saga, range delete, replication
apply, or a topology change such as a shard split - is ever visible to the cursor
for the rest of its lifetime.
This sample opens a snapshot cursor, reads the first page, then mutates the tree mid-iteration (adds a new key and overwrites an existing one) and keeps paging. The snapshot cursor never observes either change, while a fresh live read does - proving the isolation boundary.
Run it
dotnet run --project samples/SnapshotCursors
Expected output
The cursor ID is a server-assigned GUID, so it differs on every run.
Silo starting... ready.
Seeding 20 keys (k:00 .. k:19), each value = "v0"...
Opened snapshot entry cursor: 02b5b5b00f594271b0a40016d99770ac
The tree state is now frozen for this cursor's lifetime.
Snapshot page 1: k:00 .. k:04
Mid-iteration writes (should be INVISIBLE to the snapshot cursor):
+ added new key k:99 = "brand-new"
~ overwrote k:10 = "MODIFIED"
What the snapshot cursor saw:
total entries = 20 (expected 20)
contains new key k:99 = False (expected False)
value of k:10 = "v0" (expected "v0")
What a fresh live read sees (for contrast):
CountAsync() = 21 (expected 21)
value of k:10 = "MODIFIED" (expected "MODIFIED")
Snapshot isolation held: True
Done: the snapshot cursor never observed writes made after it was opened.
When to use
- Long-running exports, audits, or reports that must reflect a single instant, even though the tree keeps changing while you page through it.
- Strict isolation against every concurrent write and saga, not just a stable saga-decision view.
When not to use
- Pagination where the latest writes should appear on later pages: use a
live cursor (
OpenEntryCursorAsync/ see DurableCursors). - A single exact count that need not be one instant's view of the whole tree:
CountAsync(see StronglyConsistentScans) is cheaper than opening and draining a snapshot cursor. - A scan that pauses for hours, or a tree whose largest shard is too big to
freeze. An open is refused with
LatticeSnapshotReplayBudgetExceededExceptionwhen the deepest shard's frozen baseline would exceedLatticeOptions.MaxSnapshotReplayEntries(10,000,000 rows by default), and a cursor whose baseline can no longer be loaded - for example one left idle longer thanLatticeOptions.SnapshotBaselineTtl(6 hours by default) - throwsLatticeSnapshotExpiredExceptionon its next page and must be reopened.