Giving AI Agents Persistent Memory & State via microVM Snapshots
When someone says they want their AI agent to "have memory," I've learned to ask a follow-up question, because the word is doing double duty. Half the time they mean the agent should remember facts — what the user told it last week, a summary of a long document, a preference it learned. The other half, they mean something much more literal: the agent was halfway through a task, running commands, editing files, with a virtualenv built and a dataset downloaded — and they want it to pick up exactly there tomorrow, not start over. Those are two completely different problems, and conflating them is how you end up with an over-engineered vector store that still can't resume a job.
I'm Ajay — I build PandaStack, an open-source Firecracker microVM platform, so I spend my time on the second kind of memory: the machine the agent is actually working on. This post is about persisting an agent's execution environment — its filesystem, its installed tools, its scratch files, its working directory, its warm caches — so it can stop and later resume exactly where it was. The mechanism is almost boringly direct: a microVM snapshot is a perfect materialization of "this machine, as it was at this instant," RAM and disk together. Snapshot the agent's sandbox and you've frozen its whole working state. Restore it and you have that machine back.
Two kinds of memory people keep conflating
It's worth being precise, because the two "memories" live at different layers and want different tools. Getting this wrong is the root of a lot of wasted effort.
The first is semantic, long-term memory: the facts and context an agent accumulates. "The user prefers metric units." "Here's a summary of the 200-page contract." "Last session we decided to use Postgres." This is application-level state, and it belongs in a store built for retrieval — a vector database, a notes table, a RAG index. It's small, structured, queryable, and it survives independently of any particular machine. This post is not about that layer; it's a solved and well-covered problem, and you should reach for the usual suspects there.
The second is execution state — the working state of the machine the agent runs commands on. This is the filesystem it's been editing, the tools and packages it `pip install`ed, the half-written scratch files in `/tmp`, the current working directory, the environment variables it exported, the database it seeded, the build cache that's warm, and — if you capture RAM too — the exact state of any running processes. None of this fits in a vector DB. It isn't "facts"; it's a computer mid-task. And this is precisely what a snapshot captures.
A snapshot IS the execution memory
Here's the load-bearing idea. A Firecracker microVM snapshot captures three things together, at one instant: the guest's RAM, the guest's device/CPU state, and the rootfs disk. That's the whole machine — not a description of it, the machine itself, byte for byte. Restoring that snapshot doesn't reconstruct an approximation of where the agent was; it gives you that exact computer back, with the same files on disk and, because RAM is captured too, the same processes and in-memory state resumed mid-flight. There's no "replay the steps to get back to where we were," because you never left — you froze.
That's a fundamentally cleaner model than the usual approaches to "persist my agent's state." You don't serialize a carefully-chosen subset of state into JSON and hope you captured everything that mattered. You don't re-run an idempotent setup script and pray it lands in the same place. You capture the whole machine and put it back. If the agent had a subtle warm cache, a background process, a file it forgot it wrote — it's all there, because "all of it" is exactly what a snapshot is.
The mechanism that makes this cheap is copy-on-write. On restore, PandaStack maps the snapshot's memory `MAP_PRIVATE`, so the guest shares the snapshot's pages read-only until it writes one, at which point the kernel copies just that page. You're not duplicating gigabytes of RAM to bring a machine back; you're lazily paging it in. The restore step itself is around 49ms, and an end-to-end create-from-snapshot is p50 179ms and p99 about 203ms. Only the very first cold boot of a template — before any snapshot exists — costs around 3s. After that, resuming a frozen agent is sub-second. (For the full mechanics, see /blog/snapshot-and-fork-explained.)
Hibernate and wake: stop the agent, resume it later
The everyday use of snapshot-as-memory is hibernate/wake. An agent finishes a phase of work, or goes idle waiting on a human, or you simply want to stop paying for a VM that's doing nothing. Instead of destroying it and losing everything, you hibernate it: PandaStack snapshots the machine and pauses it. The RAM and disk are frozen to storage; the running VM stops consuming CPU and memory. Later — an hour, a day, whenever the next request arrives — you wake it, which resumes the VM from that snapshot, and the agent continues from the exact instruction it was on, with every file and cache intact.
This is the difference between "the agent's session is a fragile thing I must keep a VM alive to preserve" and "the agent's session is a durable object I can put down and pick up." A long-running research agent can hibernate overnight and wake tomorrow with its downloaded corpus, its parsed indexes, and its in-progress notebook exactly as it left them. A per-user coding agent can sleep between the user's visits and cost effectively nothing while idle, then wake warm on the next message rather than cold-booting and re-cloning the repo. (More on the always-on-but-cheap pattern in /blog/long-running-sandboxes-for-ai-agents.)
from pandastack import Sandbox
# --- Session 1: the agent does real work, then we freeze it ---------------
sbx = Sandbox.create(template="agent", ttl_seconds=3600)
# The agent builds up EXECUTION STATE: installs tools, writes files,
# warms a cache, clones a repo. None of this is "facts" for a vector DB;
# it's the machine mid-task.
sbx.exec("git clone https://github.com/acme/project /workspace/project")
sbx.exec("cd /workspace/project && python3 -m venv .venv && "
".venv/bin/pip install -r requirements.txt")
sbx.filesystem.write("/workspace/project/NOTES.md", "phase 1 done; resume at step 4\n")
# Freeze the WHOLE machine (RAM + disk) and pause it. The VM stops
# consuming CPU/memory; nothing is lost.
snap = sbx.hibernate() # snapshot + pause
print("hibernated:", snap.id) # a durable handle to this exact machine
# ... hours or days pass. We were paying ~nothing while it slept. ...
# --- Session 2: wake it and continue EXACTLY where it left off -----------
sbx.wake() # resume from the snapshot
# The venv is still built, the repo is still cloned, NOTES.md is still there,
# the working directory and warm caches are intact. No re-setup, no replay.
print(sbx.filesystem.read("/workspace/project/NOTES.md"))
sbx.exec("cd /workspace/project && .venv/bin/python run.py --resume")Snapshotting explicitly: checkpoints you can return to
Hibernate/wake is the "put it down, pick it up" flow. Sometimes you want something subtly different: a named checkpoint of the machine that you keep while the agent keeps running — a savepoint you can restore to later if a subsequent step goes wrong. That's a plain snapshot without the pause. Take a snapshot before the agent attempts a risky, hard-to-undo operation (a big refactor, a destructive migration, a long generation), and if it makes a mess, you restore the snapshot and you're back to a known-good machine, every file and process as it was at the checkpoint.
The mental model is git for the whole machine rather than for a working tree. A commit records your files; a snapshot records your files and your RAM and your running processes. "Restore to the snapshot" is a true undo of everything the agent did after that point, not just the tracked files. This is what makes snapshots more than a backup: a backup protects your data, a snapshot protects your entire in-flight computation.
from pandastack import Sandbox
sbx = Sandbox.create(template="agent", ttl_seconds=3600)
sbx.exec("cd /workspace && git clone https://github.com/acme/service .")
# Checkpoint the machine BEFORE a risky, hard-to-undo step.
checkpoint = sbx.snapshot(label="pre-refactor") # RAM + disk, frozen
# Let the agent attempt something destructive.
sbx.exec("cd /workspace && ./agent-run --refactor --aggressive")
if not tests_pass(sbx):
# Restore the WHOLE machine to the checkpoint — a true undo of every
# file, process, and byte of RAM changed after the snapshot.
sbx = Sandbox.restore(checkpoint.id)
# We're back to 'pre-refactor', exactly. Try a different approach.Forking a snapshot: branch the agent's state
Restore gives you one machine back. Fork gives you many. A fork is a copy-on-write clone of a running (or snapshotted) machine's RAM and disk — so from a single frozen agent state, you can spawn several live copies, each starting from that exact point and then diverging independently. This is the move that turns "persist my agent" into "branch my agent." You checkpoint an agent at a decision point, fork it N ways, and let each fork try a different approach — a different prompt, a different tool, a different hypothesis — all starting from identical, fully-warmed state.
Because the fork shares the parent's memory and disk copy-on-write, you're not paying to rebuild each branch's environment from scratch — the expensive setup happened once, before the fork, and every branch inherits it for free until it writes. A same-host fork lands in roughly 400–750ms; a cross-host fork in about 1.2–3.5s. That's cheap enough to fork speculatively: try five approaches in parallel, keep the one that worked, throw the rest away. This is the substrate for a whole family of best-of-N and tree-of-thought agent patterns — explored in depth in /blog/snapshot-fork-tree-of-thought.
from pandastack import Sandbox
# Get the agent to a fully-warmed decision point, then freeze it.
base = Sandbox.create(template="agent", ttl_seconds=3600)
base.exec("cd /workspace && git clone https://github.com/acme/bug-repro . && "
"python3 -m venv .venv && .venv/bin/pip install -e .")
checkpoint = base.snapshot(label="repro-ready")
# Branch the SAME warm state N ways. Each fork is a CoW clone of the
# machine's RAM + disk — the venv and repo are already there, for free.
strategies = ["patch-a", "patch-b", "patch-c"]
forks = [Sandbox.fork(checkpoint.id, metadata={"strategy": s}) for s in strategies]
results = []
for fork, strategy in zip(forks, strategies):
fork.exec(f"cd /workspace && ./try-fix.sh {strategy}")
ok = fork.exec("cd /workspace && pytest -q").exit_code == 0
results.append((strategy, ok, fork))
# Keep the branch that worked; discard the rest. Each fork was a private,
# disposable copy of the agent's state that diverged independently.
winner = next(f for s, ok, f in results if ok)
for s, ok, f in results:
if f is not winner:
f.destroy()When data must outlive the VM: durable volumes
Snapshots are the right tool for "the whole machine, exactly as it was." But sometimes you specifically want data to outlive any particular VM — you don't want it entangled with a machine snapshot; you want it to persist on its own and be attachable to whatever VM comes next. That's what durable volumes are for. A durable volume is storage that lives independently of the sandbox's ephemeral rootfs; the VM can be destroyed and the volume's data remains, ready to mount into a fresh one.
The distinction matters for agents that produce artifacts you can't afford to lose to a snapshot lifecycle: a growing dataset, a database the agent maintains, generated outputs a user will come back for. You keep the agent's transient working state in snapshots (cheap to fork, fine to discard) and its durable results on a volume (persists no matter what happens to the VM). The clearest example is a managed database: on PandaStack a managed database is a dedicated Firecracker microVM plus its own durable volume — so the Postgres data survives independently of the VM's lifecycle, created in 30–90s (the create blocks until Postgres is genuinely accepting connections). If your agent needs real, durable, per-tenant SQL state rather than a scratch machine, that's the shape to reach for. (See /blog/per-tenant-database-isolation for the isolation model.)
Which kind of state goes where
The whole design comes down to routing each piece of state to the layer built for it. Three types, three homes:
- App-level memory (vector DB / notes / RAG) — the facts and context an agent should recall: user preferences, document summaries, learned decisions. Small, structured, queryable, machine-independent. Lives in a retrieval store, not on any VM. This is the 'what the agent knows' layer, and it's a solved problem you should reach for the usual tools on.
- Execution state (snapshot) — the working state of the machine mid-task: filesystem, installed tools, scratch files, working directory, warm caches, and (with RAM captured) running processes. Only makes sense as 'the machine, exactly as it was.' Captured by a microVM snapshot; restored sub-second via copy-on-write; forkable to branch the state. This is the 'where the agent was' layer — the subject of this post.
- Durable data (volume) — results that must outlive any particular VM: a growing dataset, a maintained database, generated artifacts a user returns for. Persists independently of the sandbox lifecycle and mounts into whatever VM comes next. A managed database (dedicated microVM + durable volume) is the canonical form. This is the 'what the agent must not lose' layer.
One cryptographic caveat worth knowing
There's a sharp edge that comes free with "restore gives you the exact same machine." If you restore or fork the same memory snapshot into many guests, they all resume with identical in-memory RNG and clock state until something reseeds them — because they are, quite literally, the same machine's RAM copied N times. For most agent work that's harmless. But if you snapshot an agent that's holding secrets or key material in memory, or one that's about to generate cryptographic randomness, be deliberate: reseed the RNG and refresh anything time-sensitive on restore, and don't treat two forks of the same snapshot as independent sources of randomness. If your snapshots may carry secrets, /blog/firecracker-snapshot-secrets-security covers the handling in detail.
Putting it together
"Give my agent memory" is really two requests wearing one coat. The facts — what the agent knows — belong in a vector DB or notes store, and that's a well-trodden path. The machine — where the agent was, mid-task, with its files and tools and caches and running processes — belongs in a snapshot, because a microVM snapshot is the one thing that captures a whole machine exactly and hands it back later. Hibernate to put an agent down and wake it warm; snapshot to checkpoint before a risky step; fork to branch one warmed-up state into many; and lean on durable volumes (a managed database being the canonical case) for the data that must outlive any VM. Copy-on-write keeps all of it sub-second and cheap. The result is an agent that can genuinely stop and resume exactly where it was — not one that painstakingly rebuilds an approximation of where it thinks it might have been.
Frequently asked questions
What's the difference between an agent's semantic memory and its execution state?
They're two different layers people conflate. Semantic (long-term) memory is the facts and context an agent should recall — user preferences, document summaries, learned decisions — which is small, structured, and queryable, so it lives in a vector database or notes store, independent of any machine. Execution state is the working state of the machine the agent runs on: its filesystem, installed tools, scratch files, working directory, warm caches, and (if RAM is captured) running processes. That doesn't fit in a vector DB — it only makes sense as 'the machine, exactly as it was,' which is what a microVM snapshot captures. This post is about the second kind.
What exactly does a microVM snapshot capture?
A Firecracker snapshot captures three things together at one instant: the guest's RAM, the guest's CPU/device state, and the rootfs disk. That's the whole machine, byte for byte — not a description of it. Restoring the snapshot gives you that exact computer back, with the same files on disk and, because RAM is captured, the same in-memory state and running processes resumed mid-flight. On PandaStack the restore is made cheap by copy-on-write: memory is mapped MAP_PRIVATE so pages are shared until written. The restore step is around 49ms and an end-to-end create-from-snapshot is p50 179ms (p99 about 203ms); only the very first cold boot of a template costs around 3s.
How do hibernate and wake let an agent resume where it left off?
Hibernate is snapshot plus pause: PandaStack freezes the whole machine (RAM and disk) to storage and pauses the VM, so it stops consuming CPU and memory while nothing is lost. Wake is resume: it brings the VM back from that snapshot, and the agent continues from the exact instruction it was on, with every file, cache, and process intact — no re-setup and no replaying steps. This is what lets a long-running agent sleep overnight and wake warm the next day, or a per-user agent cost effectively nothing while idle and resume instantly on the next message, rather than cold-booting and rebuilding its environment.
How does forking a snapshot let me branch an agent's state?
A fork is a copy-on-write clone of a running or snapshotted machine's RAM and disk. From one frozen agent state you can spawn several live copies, each starting from that exact point and then diverging independently — so you checkpoint the agent at a decision point, fork it N ways, and let each branch try a different approach from identical, fully-warmed state. Because the forks share the parent's memory and disk copy-on-write until they write, you don't rebuild each environment from scratch: the expensive setup happened once before the fork. A same-host fork lands in roughly 400–750ms and a cross-host fork in about 1.2–3.5s, cheap enough to fork speculatively for best-of-N and tree-of-thought patterns.
When should I use a durable volume instead of a snapshot?
Use a snapshot when you want the whole machine preserved exactly as it was — the agent's in-flight execution state, forkable and fine to discard. Use a durable volume when you specifically want data to outlive any particular VM: a growing dataset, a database the agent maintains, generated artifacts a user will return for. A durable volume persists independently of the sandbox's ephemeral rootfs and mounts into whatever VM comes next. The canonical form is a managed database — on PandaStack a dedicated Firecracker microVM plus its own durable volume, created in 30–90s — so the data survives independently of the VM's lifecycle. Most agents use both: snapshots for transient working state, volumes for durable results.
Is it safe to restore the same agent snapshot into many VMs?
Mostly yes, with one caveat. Because a restore or fork copies the same machine's RAM, every guest resumes with identical in-memory RNG and clock state until something reseeds it. For typical agent work that's harmless. But if you snapshot an agent holding secrets or key material in memory, or one about to generate cryptographic randomness, be deliberate: reseed the RNG and refresh anything time-sensitive on restore, and don't treat two forks of the same snapshot as independent randomness sources. If your snapshots may carry secrets, handle them explicitly rather than assuming each restored copy is cryptographically independent.
49ms p50 cold start. Fork, snapshot, and scale to zero.