Runtime bills a pipeline for the CPU its transforms use, not for idle cores. A 1 vCPU, 2 GiB sandbox bills $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour of memory. Runtime was 42% to 88% cheaper than fourteen other sandbox providers on a mostly-waiting job. The rates were checked on 25 October 2026 (the comparison). Ten thousand batches a day cost $3.96, worked out below.
Why a pipeline needs a sandbox at all
Pipelines now ask a model to write the step that cleans a vendor's CSV, maps a new schema onto an old one, or extracts fields from free text. That code is untrusted under any reasonable definition. The model can be wrong, and a prompt hidden in the data it reads can steer it. Running the code on the ETL worker gives it the worker's credentials, its network and every other batch on that machine.
A sandbox per batch removes all three. The code sees one input file, writes one output file, and has no route out. If it loops, crashes or deletes everything it can reach, the loss is one throwaway machine.
The core loop
Upload the batch, write the model's transform next to it, run it with a time limit, and read the result. The output check runs on your side, where the model cannot change it.
TypeScriptimport { Runtime } from "withruntime";type Batch = { id: string; csv: string; transform: string }; // transform: Python the model wroteconst runtime = new Runtime({ waitForCapacityMs: 600_000 });async function runBatch(batch: Batch) { await using sbx = await runtime.sandboxes.create({ vcpu: 1, memoryMiB: 2048, network: { internet: false }, timeoutSeconds: 600, onLeaseEnd: "stop", labels: { pipeline: "vendor-feed", batch: batch.id }, }); await sbx.files.write("/workspace/in.csv", batch.csv); await sbx.files.write("/workspace/transform.py", batch.transform); const run = await sbx.exec(["python3", "transform.py", "in.csv", "out.csv"], { timeoutMs: 120_000, }); if (run.exitCode !== 0 || run.timedOut) return { id: batch.id, ok: false, error: run.stderr }; const out = await sbx.files.readText("/workspace/out.csv"); const header = out.split("\n")[0]; return { id: batch.id, ok: header === "customer_id,amount_usd,date", rows: out };}const batches: Batch[] = []; // today's workconst results = await Promise.all(batches.map(runBatch));console.log(results.filter((r) => r.ok).length, "of", results.length, "passed");Pythonfrom concurrent.futures import ThreadPoolExecutorfrom withruntime import Runtimeruntime = Runtime(wait_for_capacity=600)EXPECTED = "customer_id,amount_usd,date"def run_batch(batch): """batch: dict with id, csv and transform (Python the model wrote).""" with runtime.sandboxes.create( vcpu=1, memory_mib=2048, network={"internet": False}, timeout_seconds=600, on_lease_end="stop", labels={"pipeline": "vendor-feed", "batch": batch["id"]}, ) as sbx: sbx.files.write("/workspace/in.csv", batch["csv"]) sbx.files.write("/workspace/transform.py", batch["transform"]) run = sbx.exec(["python3", "transform.py", "in.csv", "out.csv"], timeout_ms=120_000) if run.exit_code != 0 or run.timed_out: return {"id": batch["id"], "ok": False, "error": run.stderr} out = sbx.files.read_text("/workspace/out.csv") return {"id": batch["id"], "ok": out.split("\n")[0] == EXPECTED, "rows": out}batches = [] # today's workwith ThreadPoolExecutor(max_workers=50) as pool: results = list(pool.map(run_batch, batches))print(sum(r["ok"] for r in results), "of", len(results), "passed")When a batch fails, send the model its stderr and the first rows of input. Then
let it write a new transform. The failed attempt cost a fraction of a cent and
left no resources in use.
Keep the code generic and the data separate
The transform takes its paths as arguments rather than hard-coding them. That lets one reviewed transform run over thousands of batches without a new model call for each. A useful rule is to cache transforms by the input's schema. A column list that the model has already handled reuses its code. Only a new shape goes back to the model.
pandas 2.1.4 and NumPy 1.26.4 are in the default image, with Python 3.12 (the sandbox environment). For polars, DuckDB or pyarrow, build an image once, then start every batch from it:
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "etl", recipe: { pip: ["polars", "duckdb", "pyarrow"] } });await using sbx = await runtime.sandboxes.create({ image: "etl", network: { internet: false } });Building is free. The stored image is billed at $0.08 per GB per 30-day month (custom images).
Large inputs and buckets
- Files of any size: uploads are split into 1 MiB pieces that travel side by
side. Each piece is verified with SHA-256, and an interrupted upload resumes
where it stopped if the connection drops. Raise
diskMiBfor inputs larger than the default disk. - Data already in S3, R2 or Google Cloud Storage: mount the bucket as a directory. Mount it read-only if the transform should not write back. The access key stays outside the guest. Runtime's egress proxy signs each request with it on the way out (mount your own bucket). The bucket's host must be on the sandbox's allow list.
- Output too large to read in one call: write it to a file, then fetch it
with
files.readorfiles.download. Do not send it throughstdout, which keeps 64 KiB.
What a pipeline step needs
| Need | How Runtime covers it |
|---|---|
| Code that cannot leak the data | network: { internet: false }, enforced on the host, not in the guest |
| One batch cannot touch another | A Firecracker microVM with its own kernel and disk per batch |
| A transform that never finishes | timeoutMs returns timedOut: true; timeoutSeconds stops the machine |
| Thousands of batches a day | 100 sandboxes at the same time on a paid account; creates queue for room |
| Retries without duplicates | An idempotency key on every write, so a retried create never makes two |
| Bounded spend | A daily spending limit per key, and maxTotalCostMicros on a sandbox |
| Finding a failed batch later | Labels on every sandbox, and lifecycle events kept 14 days |
What it costs
Take 10,000 batches a day. Each runs on a 1 vCPU, 2 GiB sandbox for 45 seconds. Of that time, its transform keeps the CPU busy for 30 seconds:
- CPU
- 10,000 × 30 s / 3,600 × $0.025
- $2.08
- Memory
- 10,000 × 45 s / 3,600 × 2 GiB × $0.0075
- $1.88
- Total
- $3.96
That is about 40 cents per thousand batches. The CPU floor, a twentieth of a vCPU, would bill 2.25 CPU-seconds for a 45-second run. The measured 30 seconds is what counts (pricing). Trying it costs nothing. A new account has 100 free hours of a 1 GB machine in its first 30 days and needs no card.
Start
Terminalnpx withruntime sandboxes run --no-internet -- python3 -c 'import pandas; print(pandas.__version__)'Approve the link that it prints in your browser. Then move to the Python or JavaScript SDK.
Related: data analysis agent, run untrusted LLM code safely, turn off a sandbox's internet, PDF processing, sandbox cost calculator.
Facts on this page were checked on 26 October 2026.