How to train small machine learning models on CPU sandboxes
Build an image with scikit-learn or CPU-only PyTorch, train in a sandbox with every vCPU busy, and copy the model file out at the end.
On Runtime a 45-minute training run on 4 vCPU and 8 GiB costs 12 cents with all four cores busy, at the rates in force on 25 September 2026, and the sandbox stops billing the moment the run ends. Runtime runs on CPUs only, with no GPU sandboxes, so it fits classical models such as gradient-boosted trees and linear models, and small neural networks, rather than large deep learning models.
The short answer
Build the image once, upload the data, train, and download the model:
TypeScriptimport { readFile, writeFile } from "node:fs/promises";import { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "train", recipe: { pip: ["scikit-learn", "joblib"] } }); // onceawait using sbx = await runtime.sandboxes.create({ image: "train", vcpu: 4, memoryMiB: 8192, network: { internet: false }, // training needs nothing from outside});await sbx.files.write("/workspace/data/train.csv", await readFile("train.csv"));await sbx.files.write( "/workspace/train.py", [ "import joblib, pandas as pd", "from sklearn.ensemble import HistGradientBoostingClassifier", "from sklearn.model_selection import train_test_split", "df = pd.read_csv('data/train.csv')", "X, y = df.drop(columns=['label']), df['label']", "X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=0)", "model = HistGradientBoostingClassifier(max_iter=300).fit(X_tr, y_tr)", "print('accuracy', model.score(X_te, y_te))", "joblib.dump(model, 'model.joblib')", ].join("\n"),);const run = await sbx.exec("python3 train.py", { check: true, timeoutMs: 3_600_000 });console.log(run.stdout);await writeFile("model.joblib", await sbx.files.read("/workspace/model.joblib"));Pythonfrom withruntime import RuntimeTRAIN = """import joblib, pandas as pdfrom sklearn.ensemble import HistGradientBoostingClassifierfrom sklearn.model_selection import train_test_splitdf = pd.read_csv('data/train.csv')X, y = df.drop(columns=['label']), df['label']X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=0)model = HistGradientBoostingClassifier(max_iter=300).fit(X_tr, y_tr)print('accuracy', model.score(X_te, y_te))joblib.dump(model, 'model.joblib')"""runtime = Runtime()runtime.images.build(name="train", recipe={"pip": ["scikit-learn", "joblib"]}) # oncewith runtime.sandboxes.create(image="train", vcpu=4, memory_mib=8192, network={"internet": False}) as sbx: with open("train.csv", "rb") as file: sbx.files.write("/workspace/data/train.csv", file.read()) sbx.files.write("/workspace/train.py", TRAIN) print(sbx.exec("python3 train.py", check=True, timeout_ms=3_600_000).stdout) with open("model.joblib", "wb") as file: file.write(sbx.files.read("/workspace/model.joblib"))pandas and NumPy are already in the default image, so the recipe adds only
what training needs. An exec may run up to 24 hours; for a run longer than
the sandbox's one-hour lease, call sbx.keepAlive() in TypeScript or
sbx.keep_alive() in Python first.
Small neural networks with PyTorch on CPU
PyTorch publishes CPU-only builds on its own package index, the right choice
for a machine with no GPU. A recipe's commands run that install while the
image builds:
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "torch-cpu", recipe: { commands: ["pip install torch --index-url https://download.pytorch.org/whl/cpu"], pip: ["numpy", "pandas"], },});await using sbx = await runtime.sandboxes.create({ image: "torch-cpu", vcpu: 4, memoryMiB: 8192 });const check = await sbx.exec( "python3 -c 'import torch; print(torch.__version__, torch.get_num_threads())'",);console.log(check.stdout);The check prints how many threads PyTorch will use; compare it with the
sandbox's vcpu so training keeps every core busy. Size the model to the
machine: a multilayer perceptron on tabular data or a small convolutional
network is a CPU job, and a model that needs a GPU to train in reasonable time
is not one for this page.
Search hyperparameters in parallel
Each trial is its own sandbox, so a search runs as wide as the account allows, up to 200 vCPUs at once on a paid account to start. Prepare one sandbox with the data on disk, then fork it: every copy starts with the files, installed packages and memory of the original, so no copy downloads the data again.
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await using base = await runtime.sandboxes.create({ image: "train", vcpu: 4, memoryMiB: 8192 });// train.py, which takes --learning-rate, and its data/ folder:await base.files.upload("./trainer", "/workspace/trainer");const rates = ["0.03", "0.1", "0.3"];const copies = await base.fork({ count: rates.length });const scores = await Promise.all( copies.map(async (copy, i) => { const run = await copy.exec(["python3", "train.py", "--learning-rate", rates[i]!], { cwd: "/workspace/trainer", timeoutMs: 3_600_000, }); await copy.stop(); return [rates[i], run.stdout.trim()]; }),);console.log(scores);Copies get the source's size and are billed as a create of that size would be; the snapshot a fork takes for itself is deleted and never charged (snapshots and forks).
Keep checkpoints and datasets
| What | Where to put it | Cost |
|---|---|---|
| A dataset used by many runs | A read-only volume copy, or your bucket mounted as /data |
Volume: 153 microdollars per GiB-hour |
| Checkpoints during a run | A read-write volume; run sync after each save |
Charged on the volume's full size |
| The finished model | files.read to your own storage |
Nothing on Runtime once stopped |
| The whole trained machine | A snapshot, kept 1 to 365 days | $0.08 per GB per 30-day month |
What CPU training needs
| Need | How Runtime covers it |
|---|---|
| ML libraries | pip packages in a recipe image; NumPy and pandas in the default image |
| All cores working | Shared CPU bursts up to vcpu; cpu: "reserved" guarantees every vCPU |
| Training data kept private | network: { internet: false }, enforced on the host |
| Parallel trials | Forks with memory and files; 100 sandboxes at once on a paid account |
| Runs longer than an hour | exec up to 24 hours with keepAlive |
| Watching the run | sbx.metrics() for measured CPU and memory |
What it costs
One run of 45 minutes on 4 vCPU and 8 GiB, with all four cores busy throughout:
TextCPU: 0.75 h × 4 vCPU × $0.025 = $0.075Memory: 0.75 h × 8 GiB × $0.0075 = $0.045Total: $0.12Fifty such runs a month cost $6.00. A run that waits on data loading uses less measured CPU and costs less, down to a floor of a twentieth of a vCPU. Runtime charges $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour of memory (pricing).
Start
Terminalnpx withruntime sandbox run --trial -- python3 -c 'import numpy, pandas; print(numpy.__version__, pandas.__version__)'Approve the connection in your browser the first time. Trial sandboxes go up to 2 vCPU and 4 GiB; add credit for 4 vCPU and more.
Related: scientific computing, data analysis agent, RL environments, Python in a sandbox.
Sources
- PyTorch: previous versions:
CPU-only wheels installed with
--index-url https://download.pytorch.org/whl/cpu, read 25 September 2026.
Facts on this page were checked on 25 September 2026.