← Engineering
Engineering · Branching

Branching running machines and keeping checkpoint history

AI agents build up state as they work: running processes, a half-built repo, a logged-in browser. You often want to try several next steps from the same point, or go back to an earlier point later. Most microVM platforms use Firecracker snapshots for this. smol machines uses a different design. This post covers how it works and where each approach is better.

TL;DR
  • Branching copies a running machine: RAM and disks, copy-on-write. The source pauses for about 80 ms, then keeps running.
  • A checkpoint captures the machine's full lineage, not one point in time. The latest checkpoint restores any earlier point, even after older checkpoint files are deleted.
  • Firecracker snapshots are cheaper to take. Disks, diff chains and clone identity are left to you.

At a glance

Firecrackersmol machines
Snapshot containsMemory, device stateMemory, device state, disks
Fork a running VMSnapshot, then restore it N timesmachine branch; source keeps running
Clone identityUp to the guest (VMGenID reseeds RNG)New name, hostname, machine id, ports
Incremental captureDirty pages onlyReads all RAM, writes only new chunks
Restore an older pointRebase base + diffs in order--at ~N on any later checkpoint
Delete older filesLater diffs stop restoringNewest checkpoint still restores all
Parent trackingNoneEvery checkpoint
HostsLinux (KVM)Linux (KVM), macOS

How Firecracker does it

Firecracker's snapshot API does one thing. You pause the VM and it writes two files: guest memory and device state. Disks are not included. The docs say they are "managed by the users".

Firecracker, abbreviated
PATCH /vm              {"state": "Paused"}
PUT   /snapshot/create {"snapshot_type": "Diff", "mem_file_path": "./diff-2.mem", ...}
PATCH /vm              {"state": "Resumed"}

# later: merge the chain back into one restorable memory file
snapshot-editor edit-memory rebase --memory-path base.mem --diff-path diff-1.mem
snapshot-editor edit-memory rebase --memory-path base.mem --diff-path diff-2.mem

Firecracker gives you the primitive and nothing else. Matching diffs to bases, keeping disks consistent with memory, and defining what a branch is: every team on Firecracker builds this itself.

Branching a running machine

In smol machines, you branch a running machine. Start it with --branchable and its RAM is backed by shared memory. When you branch, the engine pauses the source, freezes its RAM and disks into a shared base, and resumes the source on a new private layer. The child starts from the same moment. On a MacBook, the pause was about 80 ms for a 1 GiB machine.

branch
smolvm machine start  --name agent --branchable
smolvm machine branch --from agent --name try-a
smolvm machine branch --from agent --name try-b

Try it: branch the source while it runs, then write from each side.

Live branch · copy-on-write memory
source counter 0

The source machine is running. Branch it while it works.

machines
1
pages held
32
as full copies
32
still shared
0

private page (written after the branch) read through the shared base

Firecracker clones use the same copy-on-write approach. Four things are different:

Checkpoints capture the whole lineage

A branch stays on the host where it was made. A checkpoint writes the whole machine, disks included, to a file you can restore on another host with the same OS and CPU architecture.

Most snapshots capture one point in time. A smol machines checkpoint also captures its lineage. Each checkpoint records its parent: the checkpoint the machine was last captured to or restored from. Capture a machine several times and you get a chain. Restore an old checkpoint and capture again, and the history branches. Nothing is overwritten. It works like checking out an old git commit and committing on top of it.

Checkpoint lineage · click a checkpoint, then restore it
9e37793c6ef3daa66d
current position of worker
last command
smolvm machine checkpoint --name worker --store ./ckpt -o ./ckpt-3
smolvm machine checkpoint-log ./ckpt-3
~0   daa66d13daa5  worker  (this checkpoint)
~1   3c6ef3623c6e  worker
~2   9e3779b19e37  worker
inspect and rewind
smolvm machine checkpoint-log ./ckpt-3
# ~0   9f2c1a7b3e4d  worker  (this checkpoint)
# ~1   51e0d8c2a9b4  worker
# ~2   0c7a44e1fd93  worker

smolvm machine create --name rewind --from ./ckpt-3 --at ~2

Deleting old checkpoint files

Lineage only helps if old points still restore after you delete files. This is the biggest difference from a diff chain.

Delete files on both sides and see what still restores:

Three points in time · delete the older files and see what still restores

Diff chain snapshot per point

  • base.mem
    ABCDEFGH
    full snapshot
  • diff-1.mem
    C'G'
    pages dirtied since base
  • diff-2.mem
    E'H'
    pages dirtied since diff-1
  • rootfs.ext4
    disk state: yours to copy and keep consistent with each point
  1. ✓ point 1 base.mem
  2. ✓ point 2 rebase base.mem → diff-1.mem
  3. ✓ point 3 rebase base.mem → diff-1.mem → diff-2.mem

Stored checkpoints RAM + disk, retained history

  • ./ckpt-1/
    ABCDEFGH
    index + links
  • ./ckpt-2/
    ABC'DEFG'H
    index + links · retains #1
  • ./ckpt-3/
    ABC'DE'FG'H'
    index + links · retains #1, #2
objects/
ABCDEFGHC'G'E'H'
12 unique chunks stored once
  1. ✓ point 1 ./ckpt-3 --at ~2
  2. ✓ point 2 ./ckpt-3 --at ~1
  3. ✓ point 3 ./ckpt-3
Try deleting base.mem, then ./ckpt-1/ and ./ckpt-2/.

To move the history to another host, --export-from packs a checkpoint and its earlier points into one file. In our demo, three points took 98 MB, compared with 52 MB for one, because only the changes are stored. On Linux, a restore now uses the checkpoint's disk as a shared read-only base instead of copying it. On our test host, that cut restore time from 1.3 s to 0.26 s.

The trade-offs

All of this is in smolvm 1.18.0 and later.


Get the runtime on GitHub, or read how checkpoint history works.