Snapshot fidelity, and where a snapshot lives
A snapshot keeps what its fidelity names, it is as large as the memory that holds data, and for five minutes it exists on one node only.
Your agent wrote a file to /tmp, started a background process, and ended its turn. On the next turn the file is gone and the process does not exist. Nothing failed. The template asked for a snapshot of files only, and /tmp is not one of those files. A second surprise waits on the node: the snapshot that you took a minute ago is not in the object store yet.
When you finish this section, you can choose a fidelity for a template and estimate the disk that suspended actors need. You can also say where a snapshot is at each minute after a suspend.
Three fidelities
Snapshot fidelity: the set of state layers that a snapshot holds. A template names the highest level that it wants in snapshotConfig.preferredFidelity, and each level includes the levels below it.
| Fidelity | The snapshot holds | A resume |
|---|---|---|
SNAPSHOT_FIDELITY_VOLUMES | The durableDir volumes | Boots the containers from the image, with the volumes back |
SNAPSHOT_FIDELITY_ROOTFS | The volumes, and the writes to the root filesystem since boot | Boots the containers from the image, with those writes back |
SNAPSHOT_FIDELITY_MEMORY | The volumes, the root filesystem writes, and the process memory | Continues the same processes |
kagent uses the first level. Its template mounts one durableDir volume at /data and sets SNAPSHOT_FIDELITY_VOLUMES. In the first turn of a session, we told the agent to write to three places and to start a process.
RUN: mkdir -p /data/workspace && echo hello-from-turn1 > /data/workspace/note.txt; echo scratch-turn1 > /tmp/scratch.txt; echo rootfs-turn1 > /root-marker.txt 2>/dev/null; (nohup sleep 100000 >/dev/null 2>&1 &) ;…kagent suspended the actor after that turn. The second turn read the same places.
note=hello-from-turn1
scratch=cat: can't open '/tmp/scratch.txt': No such file or directory
bg=
uptime=0.74The note under /data is there. The file in /tmp is gone, the sleep process is gone, and the sandbox is 0.74 seconds old. The write to / failed in turn 1, because the harness does not run as root.
The other end is SNAPSHOT_FIDELITY_MEMORY, which our counter and our memory test used. To compare all three levels, we ran one actor under each level. The actor holds 50 MiB of random bytes and starts a child process. It keeps one count in memory, one in a file on the root filesystem, and one in a file on a durableDir volume.
| Fidelity | Memory count | Root filesystem count | Volume count | Processes after a resume | Snapshot on the node | Resume, median of 10 |
|---|---|---|---|---|---|---|
SNAPSHOT_FIDELITY_VOLUMES | back to 1 | back to 1 | continued to 25 | new, 0.2 to 0.4 s old | 44 KiB | 0.52 s |
SNAPSHOT_FIDELITY_ROOTFS | back to 1 | continued to 25 | continued to 25 | new, 0.2 to 0.3 s old | 72 KiB | 0.50 s |
SNAPSHOT_FIDELITY_MEMORY | continued to 25 | continued to 25 | continued to 25 | the same, with the same child | 56.9 MiB | 0.25 s |
We measured these three actors on the x86 server guest (24 vCPU, 47 GB, nested KVM), with gVisor and one actor for each level. Each actor got 25 requests with 13 suspends between them, and all 75 requests returned 200. The resume column is one request to the suspended actor, timed at the client, with the snapshot on the node. An asynchronous suspend took a median of 0.32 seconds at the two lower levels and 0.54 seconds at the memory level, with 10 suspends each. Raw runs
A resume at the two lower levels is a cold start of the container. The snapshot is a few KiB, so the cost is the start time of your application. Our Go actor starts in 0.3 seconds, and its resume took 0.5 seconds, twice the resume at the memory level. The kagent run was on a second x86 server guest (16 vCPU, 31 GB). There the harness image resumed in a median of 0.38 seconds under SNAPSHOT_FIDELITY_VOLUMES.
A Python agent is slower to start. We ran one on the same guest, on the cluster of the Part 9 demos, with three turns at each level. A turn after a suspend took 4.5 to 5.1 seconds under SNAPSHOT_FIDELITY_VOLUMES. It took 0.30 to 0.32 seconds under SNAPSHOT_FIDELITY_MEMORY. A stateful agent actor shows that run.
preferredFidelity is an upper limit, and a memory snapshot can restore as less. A memory snapshot records the CPU architecture, the CPU features, and the sandbox build of the worker that took it. It restores only on a worker that matches. If no such worker has room, Substrate restores the files on another worker and boots the processes new. We did not see that downgrade in any run. Write your actor so that it survives a boot from its files, also at the memory level.
A snapshot is as large as the memory that holds data
Start with the worst case, an actor whose memory is random bytes.
| Memory the actor holds | Snapshot on the node |
|---|---|
| 0 MiB | 1.3 MiB |
| 50 MiB | 51.7 MiB |
| 200 MiB | 201.9 MiB |
| 500 MiB | 502.2 MiB |
| 1,000 MiB | 1,002.7 MiB |
One file holds almost all of it.
211574784 /var/lib/ate/snapshots/a77c2723-c524-46d0-9a7e-caa97f36ab9c/local-checkpoint/…/pages.imgWe measured these sizes with du on the kind node of the x86 server guest, one actor for each size. The memory was random bytes. The snapshot folder of an actor is named by the uid of the actor. Raw runs
Random bytes are the largest snapshot that an actor of that size can have. We ran the same actor again with zero bytes, and with half zero and half random bytes.
| Memory the actor holds | Snapshot on the node | Copy in the object store | Resume from the node, median | Asynchronous suspend, median |
|---|---|---|---|---|
| 200 MiB of zero bytes | 4.0 MiB | 1.1 MiB | 0.24 s | 0.35 s |
| 500 MiB of zero bytes | 4.6 MiB | 1.1 MiB | 0.23 s | 0.35 s |
| 200 MiB, half zero and half random | 103.8 MiB | 101.1 MiB | 0.28 s | 0.66 s |
| 500 MiB, half zero and half random | 254.4 MiB | 251.2 MiB | 0.57 s | 0.79 s |
| 200 MiB of random bytes | 203.8 MiB | 201.2 MiB | 0.42 s | 0.80 s |
| 500 MiB of random bytes | 504.1 MiB | 501.3 MiB | 1.07 s | 1.07 s |
We measured six actors on the x86 server guest (24 vCPU, 47 GB, nested KVM), one for each row. Each actor had 10 resumes and 10 asynchronous suspends. All 60 resumes returned 200, and every count continued. Each size is one du reading, on the node and in the data folder of the object store. Raw runs
So the size follows the memory that holds data, and the resume time follows the same bytes. An actor with 500 MiB of zero pages resumed in 0.23 seconds, as fast as an actor that holds nothing. Do not count on that for a real heap. A Python process with a dict of one million random values reported a peak of 245 MiB. Its snapshot on the node was 215.2 MiB. Section 3.3 has its resume time.
The object store did not make the random snapshots smaller. Substrate compresses each object before the upload, and random bytes do not compress. The copy of the Python actor was 110.1 MiB in the object store, about half of its node copy. We have not found yet which step made it smaller, and that search is planned for the next edition.
| Where | With 25 suspended actors | After we deleted them |
|---|---|---|
| Snapshots on the node | 8,801 MiB | 1 MiB |
| Snapshot cache on the node | 1,765 MiB | 1,765 MiB |
| Object store | 10,824 MiB | 2,064 MiB |
We measured this footprint on the x86 server guest (24 vCPU, 47 GB, nested KVM). The node had five actors for each of the five sizes, which hold 8,750 MiB together. Each actor was 3 to 8 minutes old. The 2,064 MiB that stayed in the object store belong to golden snapshots. A template keeps its golden snapshot until you delete the template. Raw runs
An actor owns one snapshot in the object store, or two after an asynchronous suspend. A synchronous suspend deletes the earlier snapshot. An asynchronous suspend keeps it, because the new snapshot is not yet durable. The first suspend after the new snapshot is durable deletes the old one, and a delete of the actor removes both.
So plan the object store for up to two times the memory that holds data in all suspended actors. Add one golden snapshot for each template. Plan the node disk for the actors that were suspended in the last 30 minutes. Add the snapshot cache, which still held 1,765 MiB here after we deleted all 25 actors.
Two timers decide where the snapshot is
Node-local snapshot: the copy that the worker writes to the disk of its node at a suspend. atelet owns it.
Durable snapshot: the copy in the object store. Its state is SNAPSHOT_STORAGE_STATE_PENDING until the upload is complete, and then SNAPSHOT_STORAGE_STATE_COMPLETED.
After an asynchronous suspend, atelet runs two timers for each snapshot.
--snapshot-upload-delay, 5 minutes by default: the wait before the upload to the object store.--snapshot-detach-delay, 30 minutes by default: the time until atelet removes the copy on the node and clears the node assignment of the actor.
We suspended five actors and read their state every 10 seconds for 42 minutes. The fifth actor, at 1,000 MiB, never got a worker, because the pool had no room. It made no snapshot of its own, so the numbers below are for four actors.
2026-11-12T10:31:10.270Z d0 suspended-async ACTOR_STATE_SUSPENDED kind-control-plane …SNAPSHOT_OWNER_ACTOR:SNAPSHOT_STORAGE_STATE_PENDING:gen2:kind-control-plane
…
2026-11-12T10:36:01.379Z d0 state ACTOR_STATE_SUSPENDED kind-control-plane …SNAPSHOT_OWNER_ACTOR:SNAPSHOT_STORAGE_STATE_PENDING:gen2:kind-control-plane
2026-11-12T10:36:12.367Z d0 state ACTOR_STATE_SUSPENDED kind-control-plane …SNAPSHOT_OWNER_ACTOR:SNAPSHOT_STORAGE_STATE_COMPLETED:gen2:kind-control-plane2026-11-12T11:01:02.649Z d0 state ACTOR_STATE_SUSPENDED kind-control-plane …SNAPSHOT_OWNER_ACTOR:SNAPSHOT_STORAGE_STATE_COMPLETED:gen2:kind-control-plane
2026-11-12T11:01:13.670Z d0 state ACTOR_STATE_SUSPENDED - …SNAPSHOT_OWNER_ACTOR:SNAPSHOT_STORAGE_STATE_COMPLETED:gen2:-We measured four actors on the x86 server guest (24 vCPU, 47 GB, nested KVM), with the default timers. They held 0, 50, 200, and 500 MiB. The upload was complete 301 to 311 seconds after the suspend. The node copy was gone 1,801 to 1,803 seconds after the suspend. At that moment the snapshots on the node shrank from 758 MiB to 1 MiB. Each time can be up to 10 seconds late, because that was our sample interval. Raw runs
The three periods mean three different things for you.
- In the first 5 minutes, the node holds the only copy. If that copy is lost, the actor is
ACTOR_STATE_CRASHED, and the work since the last durable snapshot is gone. Section 3.5 shows that loss. - From 5 to 30 minutes, both copies exist. A resume uses the node and takes the lower line of section 3.3.
- After 30 minutes, only the object store has the snapshot. A resume downloads it first and takes about twice as long for each MiB. That number is for a bucket in the cluster, and section 8.1 shows a bucket that is 20 ms away.
The documentation promises neither the start nor the end of the upload. Treat 5 minutes as the shortest exposure, not as a limit.
Our position: do not accept the first period for state that matters. A synchronous suspend closes it for one actor. A shorter --snapshot-upload-delay on atelet makes it shorter for the cluster, and does not close it. We ran atelet with 3 seconds and 10 seconds for one test, and all 55 requests of that test succeeded. A short detach delay saves node disk and makes every resume a download.
A tag is a copy that does not depend on the node
Tag: a named copy of the durable snapshot of a suspended actor. kubectl ate create tag <name> --actor <actor> makes one. If the snapshot is not uploaded yet, the call uploads it first. kubectl ate create actor <new> --tag <name> creates a new actor that starts from the copy.
A tag has its own object in the object store, under the atespace and outside the folder of the actor. So it does not need the node of the actor, and it does not change when the actor runs on. That makes a tag the one way to move the state of an actor off a node that has stopped.
ATESPACE NAME SCOPE STATE SNAPSHOT FIDELITY AGE
lab-n 09fee120-cd19-4121-8bae-5623fcbe70eb TAG_SCOPE_PUBLISHED Ready gs://ate-snapshots/atespaces/lab-n/tags/1010ca2e-25b0-44d6-864f-a31a80da48c2 SNAPSHOT_FIDELITY_MEMORY 147m
lab-n d8328610-2fdb-4974-a6ab-7c8671586d37 TAG_SCOPE_PUBLISHED Ready gs://ate-snapshots/atespaces/lab-n/tags/8d60620d-653e-469f-9dd1-9f11a89ce7e0 SNAPSHOT_FIDELITY_MEMORY 147m
lab-n keep-t1 TAG_SCOPE_ATESPACE Ready gs://ate-snapshots/atespaces/lab-n/tags/aca8c8e0-6dfd-4ee5-9e55-ced17f7c346e SNAPSHOT_FIDELITY_MEMORY 12m
lab-n keep-u1 TAG_SCOPE_ATESPACE Ready gs://ate-snapshots/atespaces/lab-n/tags/13e4dabd-ad64-417f-95a5-ec1d5909c786 SNAPSHOT_FIDELITY_MEMORY 72s
lab-n pend-u1 TAG_SCOPE_ATESPACE Ready gs://ate-snapshots/atespaces/lab-n/tags/cfefcc57-5a75-418c-ae29-424958706266 SNAPSHOT_FIDELITY_MEMORY 69sWe measured three tags in the node-stop cases, on a second x86 server guest (16 vCPU, 23 GB), with one run for each case. A tag of an actor that was suspended with --mode sync was Ready in less than 1 s. So was a tag of an actor whose newest snapshot was still PENDING on the node, and it held that newest state. Then we stopped the node. A new actor from each tag served its first request 1.2 s after the create command, on the other node. That first request took 0.48 to 0.58 s. Raw runs
A tag has three limits. It is as old as the suspend that it copied, so later work is not in it. The actor that you create from it has a new name. And you cannot make it after the fact: create tag on an actor that was held on the stopped node failed with must be ACTOR_STATE_SUSPENDED to be tagged. Section 8.2 uses tags in its runbook for a stopped node.
Do not delete a golden tag
The golden tag of a template is in the same list. Its scope is TAG_SCOPE_PUBLISHED, and its name is a uid. kubectl ate delete tag accepts it, and our own clean-up made that mistake. It deleted every tag of the atespace, and the golden tags of two templates went with them.
## get actor-templates
ATESPACE NAME GOLDEN TAG ERROR AGE
lab-n ballast-0 d8328610-2fdb-4974-a6ab-7c8671586d37 170m
lab-n ballast-50 09fee120-cd19-4121-8bae-5623fcbe70eb 170m
## get tags
ATESPACE NAME SCOPE STATE SNAPSHOT FIDELITY AGE
## create actor from template ballast-50
Error: failed to create actor: rpc error: code = FailedPrecondition desc = Tag not found
…The template looks healthy and is broken. get actor-templates still lists the old golden tag id, and the ERROR column is empty. Each create actor --template fails with Tag not found. We saw this once, on the same guest. We made new templates to continue. Whether a golden tag can be restored is a test for the next edition.
Agent Substrate 1.0.0 · b34fbee · 2026-11-05 · Edition 2026.11