Snapshots
As a stream grows, replaying it from offset=-1 becomes expensive. Snapshots solve this by storing a compacted representation of all data up to a given offset.
PUT /{bucket}/{stream}/snapshot/{offset} publish a snapshot
PUT /{bucket}/{stream}/retention/{offset} advance retained history
GET /{bucket}/{stream}/snapshot/{offset} read a specific snapshot
HEAD /{bucket}/{stream} Stream-Snapshot-Offset names the latest snapshot
Who publishes
Snapshots are an application-level operation. Ursula stores them but does not produce them. The writer that knows how to compute the merged state is responsible for publishing. A CRDT editor, for example, periodically computes the merged document and publishes it as the latest snapshot. An agent system might snapshot accumulated tool-call state.
Application snapshots are separate from the Raft-internal snapshots Ursula takes for replication and recovery. The two never collide.
Size and content type
Snapshot bodies of up to 32 MiB are kept inline in the stream's replicated state. On a cluster with a cold backend, bodies of at least runtime.external_payload_min_size (1 MiB by default) are stored as cold-tier objects instead, up to 1 GiB: replicated state keeps only the offset, content type, digest and object reference, and reads and /bootstrap stream the body from the cold tier. A superseded cold body is deleted five minutes after the publish that replaced it, so a read that started before still completes. Through ursulagw, bodies above its --max-request-body-bytes (32 MiB by default) are refused; publish large snapshots directly to a node or raise that limit. An unset --body-budget-bytes then grows to hold one body at the limit and 32 MiB, and an explicit budget must be at least that. Give the gateway about twice the budget in memory. The snapshot has its own content type (separate from the stream's), stored as snapshot_content_type and defaulting to application/octet-stream. A binary CRDT document can be snapshotted under application/octet-stream even when the stream itself carries application/json deltas.
Checkpoint and retention
Publishing a snapshot is safe on its own: it creates a replay checkpoint but leaves all stream history readable. After the application has verified the checkpoint, it may explicitly advance retention to the same or an older checkpoint boundary with PUT /{bucket}/{stream}/retention/{offset}. Only that operation makes earlier reads return 410 Gone and makes old cold-tier data eligible for GC.
On a JSON stream, snapshot and retention offsets must be message boundaries: an offset just after a message's LF, such as a Stream-Next-Offset Ursula returned. An offset inside a message answers 400; when Ursula cannot read the byte before the offset to check it, it publishes nothing and answers 503. Retention is exact.
Immutability
Snapshots are immutable at an offset. Retrying the same body and content type is idempotent and returns the same Stream-Snapshot-Digest; different content at the same offset returns 409. Snapshots cannot be deleted. To replace a snapshot, publish a new one at a higher offset.
Related
- Bootstrap: fetch latest snapshot plus post-snapshot updates in one request
- API: publish snapshot
- API: read snapshot