Runtime

How to run LLM-generated transformation code in a data pipeline

Run each batch's model-written transform in its own sandbox with the internet off, check the output's shape, and only then load it.

Runtime charges a pipeline for the CPU its transforms burn, 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, and Runtime came out 42% to 88% cheaper than eleven other sandbox providers on a mostly-waiting job at rates checked 23 September 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 by any honest definition: the model can be wrong, and a prompt hidden in the data it read can steer it. Running it 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 back. The output check happens 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, and let it write a new transform. The failed attempt cost a fraction of a cent and left nothing behind.

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 the model has already handled reuses its code, and 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 and 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, are verified with SHA-256 and pick up where they stopped if the connection drops. Raise diskMiB for inputs larger than the default disk.
  • Data already in S3, R2 or Google Cloud Storage: mount the bucket as a directory, 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 has to be on the sandbox's allow list.
  • Output too big to read in one call: write it to a file and fetch it with files.read or files.download, not through stdout, 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; the lease stops the machine
Thousands of batches a day 100 sandboxes at once on a paid account to start; 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 maxCostMicros on a create
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 on a 1 vCPU, 2 GiB sandbox that runs for 45 seconds, of which its transform keeps the CPU busy for 30:

TextCPU:    10,000 × 30 s / 3,600 × $0.025            = $2.08Memory: 10,000 × 45 s / 3,600 × 2 GiB × $0.0075   = $1.88Total:                                              $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, so the measured 30 is what counts (pricing). Trying it costs nothing: a new account has 50 sandbox hours free and needs no card.

Start

Terminalnpx withruntime sandbox run --trial --no-internet -- python3 -c 'import pandas; print(pandas.__version__)'

Approve the link 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 25 September 2026.