Skip to content
Runtime

How to run SWE-bench-style agent evals in parallel sandboxes

Build each task's environment once. Start one isolated sandbox per attempt, apply the agent's patch, run the tests, and record pass or fail.

On Runtime, every attempt gets its own machine. A paid account runs 100 sandboxes at once. Each sandbox is a Firecracker microVM with its own kernel, so one task's tests cannot disturb another task's. Runtime bills the CPU the tests use, not the CPUs they hold. 500 ten-minute attempts on 2 vCPUs and 4 GiB cost $2.92 at Runtime's rates. The calculation is below.

What a SWE-bench-style eval is

SWE-bench gives a language model "a codebase and an issue". It asks the model to generate "a patch that resolves the described problem". The benchmark's harness applies the patch to the repository. Then it runs the repository's tests, in Docker, to decide whether the issue is resolved. Each task names a repository, a base commit, the problem statement, and two lists of tests. FAIL_TO_PASS lists the tests that the fix must make pass. PASS_TO_PASS lists the tests that must keep passing. The full dataset has 2,294 tasks. SWE-bench Verified has 500 (as SWE-bench's documentation states, read 26 October 2026).

SWE-bench's own Docker setup guide asks for at least 120 GB of free disk. It recommends 16 GB of RAM and at least 8 CPUs for its evaluation. The harness runs tasks side by side with --max_workers. On one machine, parallelism stops where that machine's disk and cores do. In sandboxes, each attempt has its own disk and cores.

The same pattern fits any eval in which an agent changes code and tests judge the result. Each eval needs one environment per task, one attempt, one check and one score.

The core loop

Give every task an image with its repository checked out at the base commit and its dependencies installed. Then start one sandbox per attempt:

TypeScriptimport { Runtime } from "withruntime";type Task = { id: string; image: string; patch: string; testCommand: string };const tasks: Task[] = []; // load your task list here// Past the account's limit, a create waits for room instead of failing.const runtime = new Runtime({ waitForCapacityMs: 600_000 });async function evaluate(task: Task) {  await using sbx = await runtime.sandboxes.create({    image: task.image,    labels: { run: "eval-2026-09-25", task: task.id },    network: { internet: false },    timeoutSeconds: 1800,    onLeaseEnd: "stop",  });  await sbx.files.write("/workspace/agent.patch", task.patch);  const apply = await sbx.exec("git apply /workspace/agent.patch", { cwd: "/workspace/repo" });  if (apply.exitCode !== 0) return { id: task.id, resolved: false, reason: apply.stderr };  const tests = await sbx.exec(task.testCommand, { cwd: "/workspace/repo", timeoutMs: 900_000 });  return { id: task.id, resolved: tests.exitCode === 0, timedOut: tests.timedOut };}const results = await Promise.all(tasks.map(evaluate));console.log(results.filter((r) => r.resolved).length, "of", results.length);
Pythonfrom concurrent.futures import ThreadPoolExecutorfrom withruntime import Runtimetasks = []  # dicts with id, image, patch, test_commandruntime = Runtime(wait_for_capacity=600)  # past the limit, a create waits for roomdef evaluate(task):    with runtime.sandboxes.create(        image=task["image"],        labels={"run": "eval-2026-09-25", "task": task["id"]},        network={"internet": False},        timeout_seconds=1800,        on_lease_end="stop",    ) as sbx:        sbx.files.write("/workspace/agent.patch", task["patch"])        apply = sbx.exec("git apply /workspace/agent.patch", cwd="/workspace/repo")        if apply.exit_code != 0:            return {"id": task["id"], "resolved": False, "reason": apply.stderr}        tests = sbx.exec(task["test_command"], cwd="/workspace/repo", timeout_ms=900_000)        return {"id": task["id"], "resolved": tests.exit_code == 0, "timed_out": tests.timed_out}with ThreadPoolExecutor(max_workers=100) as pool:    results = list(pool.map(evaluate, tasks))print(sum(r["resolved"] for r in results), "of", len(results))
  • The sandbox is deleted when its block ends, even when a test crashes.
  • A test that hangs returns timedOut: true with its output so far. It is a result, not an exception.
  • With the internet off, a task cannot fetch its answer or leak the test set.
  • Labels mark every sandbox with its run and task. Then runtime.sandboxes.list({ labels: { run: "eval-2026-09-25" } }) finds them.

Build the task environments

A task image can come from any container image, public or private. It can also come from a Dockerfile with its build context, or from a short recipe. Build each image once. Every attempt starts from it without reinstalling:

TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({  name: "task-env",  recipe: {    apt: ["libpq-dev"],    commands: [      "git clone https://github.com/your-org/your-repo.git repo",      "cd repo && git checkout 0123abc && pip install -e .",    ],  },});

Building is free. A stored image is charged by its size, at $0.08 per GB per 30-day month. An image can be up to 20 GiB (custom images). If your harness already ships a Docker image for each task, pass it as image and use it as it is.

Many attempts at one task

For pass@k, let the agent work in one prepared sandbox. Or start it from the task image, then fork it. fork copies the machine as it is (files, memory and running processes) into 1 to 100 running sandboxes:

TypeScriptimport { Sandbox } from "withruntime";await using base = await Sandbox.create({ image: "task-env", network: { internet: false } });const copies = await base.fork({ count: 5 });// ... run one agent attempt in each copy, then score each ...await Promise.all(copies.map((copy) => copy.stop()));

To start attempts later, or from other processes, keep the prepared machine as a snapshot. Then create sandboxes from it with snapshot: id.

What matters for evals

Need How Runtime covers it
Tasks that cannot affect each other A Firecracker microVM with its own kernel and disk per attempt
The same environment every time Versioned images: name@version pins one build
Many sandboxes at the same time 100 sandboxes, 200 vCPUs and 400 GiB across running ones on a paid account
Bursts past the limit Creates wait for room, up to two minutes by default or as long as you set
Hung tests Per-command timeoutMs, up to 24 hours; timeoutSeconds per sandbox
No leaked answers network: { internet: false }, enforced outside the guest
Retries that do not count twice Idempotency keys on every write
Several samples per task Forks of 1 to 100 running copies; snapshots kept while you have credit
Cost you can predict Measured CPU plus reserved memory; maxTotalCostMicros per sandbox

To run more than 100 sandboxes at once, contact support with the numbers you need (pricing).

What it costs

Take 500 attempts. Each keeps a 2 vCPU, 4 GiB sandbox running for 10 minutes. Each uses 120 CPU-seconds of test work:

CPU
500 × 120 s / 3,600 × $0.025
$0.42
Memory
500 × 600 s / 3,600 × 4 GiB × $0.0075
$2.50
Total
$2.92

Runtime charges $0.025 per vCPU-hour of measured CPU, with a floor of a twentieth of a vCPU. It charges $0.0075 per GiB-hour of memory (pricing). A sandbox waiting on the agent's model costs $0.03125 an hour at this size. Your model provider bills the model calls. Runtime does not. A new account gets 100 free hours of a 1 GB machine in its first 30 days and needs no card. It runs four sandboxes at once.

Sources

Related: RL environments, coding agent sandbox, sandbox forks, sandbox cost calculator.

Facts on this page were checked on 26 October 2026.

Related.

100 free hoursin your first 30 days.

of a 1 GB machine

  • No credit card
  • Four sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Start free, no card
Start free, no card