all posts

How to persist data in a sandbox

Ajay Kumar··9 min read

A sandbox is disposable on purpose. Its filesystem is a copy-on-write clone of a template image, and when the sandbox is deleted the clone goes with it. That is exactly what you want when you are running untrusted code — no residue, no cross-contamination, no cleanup script that might miss something.

Then you need to keep something. A model generated a chart and a user wants to download it. A dependency install took four minutes and you will run it again in twenty seconds. An agent has notes it needs tomorrow. There are four distinct mechanisms for this, they have genuinely different properties, and picking the wrong one is how people end up with a 40 GiB volume holding a node_modules directory.

1. Move the file out — the default answer

Most of the time "persist this" means "one artifact needs to leave". Read it out over the filesystem API and store it wherever your application already stores things. No new infrastructure, no lifecycle to manage.

from pandastack import Sandbox

with Sandbox.create(template="code-interpreter") as sb:
    sb.filesystem.upload("sales.csv", "/home/panda/sales.csv")

    sb.run_code("""
import pandas as pd, matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

df = pd.read_csv("/home/panda/sales.csv")
df.groupby("region")["revenue"].sum().plot.bar()
plt.savefig("/home/panda/chart.png", dpi=150, bbox_inches="tight")
""")

    # The artifact leaves before the sandbox does
    sb.filesystem.download("/home/panda/chart.png", "chart.png")

The TypeScript SDK mirrors this with `sb.filesystem.upload`, `download`, `read`, and `write`. Use it whenever the thing you need is a file rather than a machine state — which is more often than people expect.

Read the artifact out before the sandbox is destroyed, not after the last command. If a TTL expires or the process exits early, `finally` blocks in your own code are the only thing standing between you and a lost result.

2. Snapshot the machine — when the state is the setup

If what you want to keep is the environment rather than a file — dependencies installed, a repo cloned, a database seeded, a browser logged in — snapshot it. A snapshot captures the sandbox at a point in time, and creating a new sandbox from it restores that exact state rather than replaying the setup.

import { Client } from "@pandastack/sdk";

const client = new Client();
const sb = await client.sandboxes.create({ template: "base" });

// Expensive one-time setup
await sb.exec("git clone --depth 1 https://github.com/acme/api /work");
await sb.exec("cd /work && npm ci");

// Freeze it — every future run starts here instead of repeating four minutes
const snap = await sb.snapshot();
await sb.kill();

This is the single highest-leverage trick in sandbox work. A test suite that spends four minutes installing before running becomes one that starts already installed. An agent that needs a logged-in browser session can resume it instead of authenticating again. And because restore reads a memory and disk image rather than booting and reinstalling, the cost of "start fresh" collapses — which in turn makes per-task isolation affordable rather than a trade-off.

Snapshots are the wrong tool for data that changes constantly. Re-snapshotting after every write is expensive and leaves you managing a chain of images, which is a database with extra steps.

3. Attach a volume — when many runs share a directory

A persistent volume is a named ext4 block device that outlives any sandbox. Attach it at create time, it mounts at `/mnt/{name}`, and whatever is written there is still there for the next sandbox that attaches it.

from pandastack import Client

client = Client()
client.volumes.create(name="agent-workspace", size_mb=8192)

sb = client.sandboxes.create(
    template="base",
    volumes=[{"name": "agent-workspace"}],
)

# /mnt/agent-workspace outlives this sandbox
sb.exec("mkdir -p /mnt/agent-workspace/notes")
sb.exec("echo 'ran the migration audit' >> /mnt/agent-workspace/notes/log.md")
sb.kill()

Volumes are the right answer for a long-running agent's working directory, a shared build cache, or anything where several sandboxes over time need the same mutable directory. They are billed on provisioned size, not on bytes written, so a 64 GiB volume holding 200 MB costs what 64 GiB costs. Size them for what you need now.

One structural caveat: a volume is a block device attached to one sandbox at a time, so it is not a shared filesystem for concurrent workers. If ten sandboxes need the same data simultaneously, that is object storage or a database, not a volume.

4. Use a database — when it is data, not files

If what you are persisting has structure, is queried, and is written by more than one thing at a time, none of the above is right. Files in a volume become a homemade database with no transactions, no concurrent access story, and no backups. Point the sandbox at a real database over the network and let it be a database.

pandastack db create --label agent-memory --size 1g
pandastack db connection <db-id>

# Pass the connection string into the sandbox as an environment variable —
# the sandbox stays disposable, the data does not live in it

This also keeps the security boundary intact. A sandbox running untrusted code should hold as little as possible: give it a scoped credential to a database that holds only its own data, and destroying the sandbox destroys nothing that matters.

Choosing between them

  • One artifact needs to leave — read it out with the filesystem API. No exceptions, no infrastructure.
  • Expensive setup repeated across runs — snapshot it, and start every run from the snapshot.
  • A mutable working directory shared across sequential sandboxes — a volume.
  • Structured, queried, concurrently written data — a database.
  • Large blobs many consumers read — object storage, with the sandbox holding only a signed URL.
A useful sanity check: if you would not put it on a laptop that gets wiped nightly, do not leave it only inside a sandbox. Sandboxes are cattle, and the entire value of the model comes from being able to destroy one without thinking.

Four things that bite

  1. Writes in flight when the sandbox dies. A TTL expiry does not wait for your file to finish flushing. Write to a temp path and rename, or copy the artifact out as a distinct step before teardown.
  2. Volume size chosen optimistically. Provisioned size is what you pay for and a volume cannot shrink. Start small; growing later is easier than explaining a storage bill.
  3. Snapshots of a running database. Freezing a machine mid-transaction gives you a snapshot that restores into recovery. Stop the service, or snapshot from a quiesced state.
  4. Secrets baked into a snapshot. Whatever was in the environment or on disk when you snapshotted is in the image, and every sandbox restored from it. Inject credentials at create time instead of capturing them.

The short version

Default to moving files out and keeping the sandbox disposable. Snapshot when the expensive thing is the setup rather than the data. Attach a volume when successive sandboxes genuinely need the same mutable directory. Use a database when the thing you are storing is data. Most of the persistence problems I see come from using a volume for something that should have been one download call, or from treating a directory of JSON files as a database that three workers write to at once.

Frequently asked questions

Does anything survive when a sandbox is deleted?

Only what you explicitly arranged to survive. The sandbox's own filesystem is a copy-on-write clone that is destroyed with it, so files under the home directory or /tmp are gone. What persists is anything you moved elsewhere first: files downloaded through the filesystem API, snapshots you created, data on an attached volume, and rows in an external database. This is deliberate — the isolation guarantee that makes it safe to run untrusted code depends on nothing outliving the sandbox by accident.

What is the difference between a snapshot and a volume?

A snapshot is a frozen copy of an entire machine at one moment: filesystem and memory state together, immutable, used to start new sandboxes from a known point. A volume is a mutable block device that attaches to whichever sandbox asks for it, keeps whatever is written, and has no notion of versions. Use a snapshot when you want every run to start identically from an expensive setup, and a volume when successive runs need to build on each other's changes. They are complementary — a sandbox can boot from a snapshot and mount a volume.

Can two sandboxes share the same volume at once?

No, and it is worth understanding why rather than looking for a workaround. A volume is a block device with an ext4 filesystem, and ext4 is not a clustered filesystem — mounting it read-write from two machines simultaneously corrupts it, which is a property of the filesystem rather than a platform restriction. For genuinely concurrent access, use something designed for it: object storage for blobs, a database for structured data, or a message queue for work handoff. Volumes are for sequential ownership, one sandbox at a time.

How do I get a large file into a sandbox quickly?

For anything beyond a few tens of megabytes, do not push it through the filesystem API — have the sandbox pull it. A signed URL from object storage, downloaded inside the sandbox with curl, takes one network hop at cloud bandwidth instead of two hops through your client. If the same large file is needed by many sandboxes, the better pattern is baking it into a template or a snapshot, so it is already present when the sandbox starts and costs nothing per run.

Is it safe to keep secrets on a volume?

It is safer than baking them into a snapshot or a template image, and less safe than not putting them in the sandbox at all. A volume is scoped to your workspace and only reachable by a sandbox you attached it to, so it is a reasonable place for state. But if code inside the sandbox is untrusted — model-generated or user-supplied — assume that anything readable inside is readable by that code. Prefer short-lived, narrowly-scoped credentials injected at create time, so a leak expires on its own rather than being permanent.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.