Runtime (withruntime.com) moves a folder into a sandbox as one compressed
archive. The archive is unpacked as it arrives. Volumes that outlive sandboxes
are billed at about $0.11 per GiB per 30-day month. Runtime mounts
your own bucket, and the sandbox never holds its key.
An agent's work begins with files it did not make and ends with files someone
else needs. Get the transfer wrong, and the result is slow starts, a server of
yours that relays gigabytes for no reason, or a results folder full of
node_modules. This post sorts an agent's files by where they come from and
how often they are reused. It gives each kind its own route.
Which route fits which files?
Two questions decide the route: how big are the files, and will the next sandbox need them too?
| Files | Route | Why |
|---|---|---|
| A user's upload, a prompt's attachment | files.write from your server |
Small, and already in your hands |
| A project folder | files.upload of the folder |
One archive, permissions and links kept |
| A public dataset or a release download | The sandbox fetches it itself | Your server never relays the bytes |
| Private data already in a bucket | A bucket mount, read-only | No copy, and no key inside the sandbox |
| Data many sandboxes read: weights, a corpus | A volume, attached read-only to each | Copied in once, read by any number |
| A cache one agent reuses run after run | A read-write volume, or an image | Survives the sandbox |
| Results someone reads | files.download of what changed |
Only the agent's output comes back |
| Results other systems read | Written to a mounted bucket | Lands where they already look |
The rest of this post covers them in the order that an agent's task uses them: in, during, and out.
How do you hand a task to the agent?
Put everything the task needs in one folder, and upload it in one call. A
folder travels as one compressed archive. The sandbox's own tar unpacks it
as it arrives, so a thousand small files cost about the same as one archive. A
single file is sent in parallel, in 1 MiB chunks. Each chunk is checked by
SHA-256, and an upload resumes after a dropped connection.
Then the agent works. At the end, you want its output, not the inputs you already have. List the folder, keep the files modified after the hand-over, and bring back only those:
TypeScriptimport { mkdtemp, writeFile } from "node:fs/promises";import { tmpdir } from "node:os";import { join } from "node:path";import { Sandbox } from "withruntime";// The task: a folder on your machine with the inputs and the instructions.const task = await mkdtemp(join(tmpdir(), "task-"));await writeFile(join(task, "sales.csv"), "region,revenue\nnorth,1200\nsouth,950\n");await writeFile(join(task, "TASK.md"), "Summarize revenue by region in out/summary.md\n");await using sbx = await Sandbox.create({ labels: { task: "revenue-summary" } });await sbx.files.upload(task, "/workspace/task"); // in: one archive, unpacked as it arrivesconst handedOver = Date.now() - 2_000; // a little slack for the two clocks// The agent works here, through exec and files calls. Its output, for example:await sbx.files.write("/workspace/task/out/summary.md", "north: 1,200\nsouth: 950\n");// Out: only what changed since the hand-over.const entries = await sbx.files.list("/workspace/task", { depth: 4 });const changed = entries.filter((e) => e.type === "file" && Date.parse(e.modifiedAt) >= handedOver);for (const file of changed) { await sbx.files.download(file.path, join(task, file.path.slice("/workspace/task/".length))); console.log("brought back", file.path, `${file.size} bytes`);}console.log(`${changed.length} of ${entries.length} entries changed`);Pythonimport osimport tempfileimport timefrom datetime import datetimefrom withruntime import Sandbox# The task: a folder on your machine with the inputs and the instructions.task = tempfile.mkdtemp(prefix="task-")with open(os.path.join(task, "sales.csv"), "w") as f: f.write("region,revenue\nnorth,1200\nsouth,950\n")with open(os.path.join(task, "TASK.md"), "w") as f: f.write("Summarize revenue by region in out/summary.md\n")with Sandbox.create(labels={"task": "revenue-summary"}) as sbx: sbx.files.upload(task, "/workspace/task") # in: one archive, unpacked as it arrives handed_over = time.time() - 2 # a little slack for the two clocks # The agent works here, through exec and files calls. Its output, for example: sbx.files.write("/workspace/task/out/summary.md", "north: 1,200\nsouth: 950\n") # Out: only what changed since the hand-over. entries = sbx.files.list("/workspace/task", depth=4) changed = [e for e in entries if e["type"] == "file" and datetime.fromisoformat(e["modifiedAt"].replace("Z", "+00:00")).timestamp() >= handed_over] for file in changed: local = os.path.join(task, file["path"].removeprefix("/workspace/task/")) os.makedirs(os.path.dirname(local), exist_ok=True) sbx.files.download(file["path"], local) print("brought back", file["path"], f"{file['size']} bytes") print(f"{len(changed)} of {len(entries)} entries changed")A time check is enough when the agent writes new files. When it edits files
in place, and you need to know which edits changed the content, compare
content hashes instead. The recorder in
see what your agent did does this. For a
code task, git diff inside the sandbox is the best option: it shows the
change as text, ready to review.
Should big data pass through your server?
No. If the data is on the public internet, let the sandbox download it directly. The data arrives faster, your server stays out of the path, and nothing large is held in your process's memory on the way:
TypeScriptimport { Sandbox } from "withruntime";const url = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet";await using sbx = await Sandbox.create({ diskMiB: 16_384, network: { internet: true, allow: ["d37ci6vzurychx.cloudfront.net"] }, // this host and no other});const r = await sbx.exec(`curl -fsSL --retry 3 -o trips.parquet '${url}' && ls -l trips.parquet`, { cwd: "/workspace", timeoutMs: 600_000,});console.log(r.exitCode, r.stdout.trim());Pythonfrom withruntime import Sandboxurl = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet"with Sandbox.create(disk_mib=16_384, network={"internet": True, "allow": ["d37ci6vzurychx.cloudfront.net"]}) as sbx: r = sbx.exec(f"curl -fsSL --retry 3 -o trips.parquet '{url}' && ls -l trips.parquet", cwd="/workspace", timeout_ms=600_000) print(r.exit_code, r.stdout.strip())Two settings in that code matter. The allow list names the one host the download needs. If a model later reads instructions inside the data, it still cannot send anything to another host. The disk is sized for the data: a sandbox's default disk is 4 GiB, and a large file plus its unpacked or converted copy fills that quickly (size CPU and memory).
What about private data in a bucket?
Mount it. A bucket in S3, R2 or Google Cloud Storage appears as a folder. The agent reads it with ordinary file tools. You store the bucket's key once, as a secret. The sandbox gets a placeholder. The real key is added to requests only on the way to the bucket's host. The agent can read the data, but it cannot read the key:
TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create();await sbx.mounts.add({ provider: "s3", bucket: "acme-research", prefix: "datasets/2026-q3", region: "us-east-1", path: "/data", secret: "RESEARCH_BUCKET", readOnly: true, // the agent reads; it cannot delete or overwrite});console.log((await sbx.exec("ls -la /data | head")).stdout);Pythonfrom withruntime import Sandboxwith Sandbox.create() as sbx: sbx.mounts.add(provider="s3", bucket="acme-research", prefix="datasets/2026-q3", region="us-east-1", path="/data", secret="RESEARCH_BUCKET", read_only=True) # the agent reads; it cannot delete or overwrite print(sbx.exec("ls -la /data | head").stdout)Keep readOnly and prefix. A prefix limits the agent to the data for this
task. Read-only means that an agent that decides to clean up cannot delete
anything in your bucket. For results that other systems pick up, mount a
second, writable prefix for output only. A mount lasts through a pause and a
wake, and it ends when the sandbox stops
(mount an S3 bucket).
When is a volume the right answer?
Use a volume when the same bytes are needed by many sandboxes, or by the next sandbox. A volume is a disk that outlives sandboxes. It has two modes:
- Read-write, one sandbox at a time. A package cache, a database file, a model's checkpoint directory. Each run picks up where the last one stopped.
- Read-only copies, any number at the same time. Model weights or a reference corpus
that every worker reads. Copy the data in once. Then attach it to a hundred
sandboxes with
mode: "snapshot". None of them can change it.
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();const weights = await runtime.volumes.create({ sizeMiB: 40_960, name: "weights-v3" });const workers = await Promise.all( [1, 2, 3, 4].map(() => runtime.sandboxes.create({ volumes: [{ volumeId: weights.id, path: "/models", mode: "snapshot" }], }), ),);console.log(workers.map((w) => w.id));await Promise.all(workers.map((w) => w.stop()));Pythonfrom withruntime import Runtimeruntime = Runtime()weights = runtime.volumes.create(40_960, name="weights-v3")workers = [runtime.sandboxes.create(volumes=[{"volumeId": weights["id"], "path": "/models", "mode": "snapshot"}]) for _ in range(4)]print([w.id for w in workers])for w in workers: w.stop()A volume is billed for its full size from the moment it is created, whether or not it holds data. Runtime backs it up off its server every day. A volume lives on one server, and a sandbox that uses it is placed on that server. Before you choose a volume, note this trade-off: a sandbox with volumes cannot be snapshotted. If your agent relies on snapshots to save and restore its sandbox, keep the reused data in a custom image or a bucket mount instead (an undo button for AI agents).
Volume, image or bucket?
All three keep data beyond one sandbox. The difference is how often it changes and who writes it:
| Keep it in | Changes | Written by | Works with snapshots |
|---|---|---|---|
| A custom image | Rarely: when you rebuild it | Your build | Yes |
| A volume | Every run, or never (read-only) | The sandbox using it | No |
| A bucket mount | Any time, from anywhere | Any system with the key | Mount it in each new sandbox |
Tools and libraries belong in an image. They change when you decide to rebuild, and every sandbox from the image starts with them installed. Data that one run writes and the next run reads belongs on a volume. Data that other systems also read or write belongs in a bucket.
How should the model itself find the files?
Tell it. The task message should name the input folder, the output folder, and
what belongs in each: "the data is in /workspace/task, write results to
/workspace/task/out, do not change the inputs". A model that has to discover
the layout spends its first turns on ls and find. A model that does not
know where output belongs scatters it across the disk. That defeats the
download step above. An agent connected over MCP reads, writes, and lists files
with runtime_sandboxes_files_read, runtime_sandboxes_files_write and
runtime_sandboxes_files_list. It can also read a range of lines from a large
file, not the whole file.
What limits should you plan around?
A few numbers save a debugging session:
- Writes outside
/workspaceare limited to 1 MiB through the files API. Write large files to/workspace, then move them with a command. - Four uploads and four downloads run at the same time per sandbox. Extra transfers wait in line. Upload a folder rather than a thousand files one by one.
- Downloads are checked. A read that arrives short is read again. If the retry is also short, the download fails instead of returning part of a file. A downloaded folder appears in place only after all of it has arrived.
- A folder download refuses links that point outside the folder so nothing in a sandbox can write anywhere else on your machine.
In short
- Upload a task's inputs as one folder, and bring back only the files that changed since the hand-over.
- Let the sandbox fetch large public data itself, with an allow list naming only the source.
- Mount private buckets read-only with a prefix; the sandbox never holds the key.
- Use a volume for data many sandboxes read or the next run reuses, and remember that a sandbox with volumes cannot be snapshotted.
- Put tools in an image, run-to-run state on a volume, and shared data in a bucket.
Run it on Runtime
Runtime costs 42% to 88% less than fourteen other sandbox providers for an agent that mostly waits on a model (compare costs). Every Runtime sandbox has a files API with checked transfers. Folder uploads travel as one archive. Bucket mounts keep keys out of the sandbox. Volumes outlive the sandbox that uses them. Read the storage guide, then create an account at withruntime.com and hand your agent its first task folder.