Forking
fork() creates N independent copies of a running sandbox. Each child
inherits the parent’s filesystem state and its running process
state at the moment of the fork. The children diverge from there.
This is the AI-agent backtracking primitive: do expensive setup once in a parent sandbox, then fork it to explore N hypotheses in parallel.
Why this is fast
The runner snapshots the parent VM once (~370 ms for a 1 GiB VM via Firecracker’s diff snapshot), then each child:
- Hardlinks the parent’s
mem.snapinto its own runDir. Same inode → Linux page cache shared across all children in the batch. 100 forks of a 1 GiB sandbox cost ~1 GiB of total page cache, not 100 GiB. - Reflinks the parent’s scratch disk via XFS
FICLONE. Instant copy-on-write at the block level. - Restores Firecracker from the snapshot in ~16 ms.
- Re-attaches to a fresh TAP device with a new guest IP — every fork has its own network identity.
The whole operation for a 5-child batch: ~490 ms total (snapshot + 5 × restore). For larger batches the per-child cost is the only thing that scales.
Use case: AI agent backtracking
Run setup once. Fork to explore hypotheses in parallel. Discard the losers, keep the winner.
from isorun import Sandbox
with Sandbox("python:3.12") as parent: parent.create()
# 30 seconds of expensive setup parent.exec("pip install transformers torch datasets") parent.exec("python download_model.py")
# Fork into 5 children — each one gets the loaded model in # memory, no need to re-load. children = parent.fork(count=5)
# Try 5 different hypotheses in parallel hypotheses = [ "lr=1e-4 batch=32", "lr=3e-4 batch=32", "lr=1e-4 batch=64", "lr=3e-4 batch=16", "lr=5e-5 batch=128", ] results = [] for child, hp in zip(children, hypotheses): out = child.exec(f"python train.py {hp}") results.append((hp, out.stdout))
# Pick the winner, destroy the rest winner = max(results, key=lambda r: parse_score(r[1])) for child in children: child.destroy()# Fork the parent into 5 childrencurl -X POST https://api.isorun.ai/v1/runs/$PARENT_ID/fork \ -H "Authorization: Bearer $ISORUN_API_KEY" \ -d '{"count": 5}'# → {# "parent": "run...",# "forks": [# {"id": "run...", "status": "running", ...},# ...# ]# }What gets inherited
Verified by the test suite — every fork sees:
- Filesystem state. Files written by the parent before the fork are visible in every child. Files written by a child are visible only to that child (CoW at the block level via reflink).
- Memory state. Pages loaded by the parent — including JIT caches, ML model weights, cached database connections, in-memory parsers — are shared across forks via the same page cache. No re-load cost.
- Running processes. A
sleep 600started by the parent is visible in every child at the same PID. The kernel’s process table is part of the snapshot. - Environment variables and cwd. Whatever the parent set is what each child sees.
What does not get inherited
- TCP connections. Each fork gets a new TAP device and a new guest IP. Established outbound connections from before the fork are reset. Listen sockets survive.
- The vsock socket the runner uses to talk to the agent. Each fork gets a fresh one (this is what the ~16 ms per-child cost is paying for).
- In-flight I/O. Open file descriptors survive but anything buffered in kernel page cache for “the wrong inode” gets consistent — children see the parent’s filesystem state at the exact moment of the fork.
Limits
| Constraint | Value |
|---|---|
| Max forks per batch | 50 |
| Source must be in state | running |
Cost of fork() operation | ~490 ms for 5 children, scales linearly past that (~16 ms/child after the snapshot) |
| Per-fork memory overhead | 5 MiB PSS (the dirty CoW pages only) |
Why this matters
Every other sandbox provider you’ve evaluated does forking by re-running the setup phase: agent_loop_iteration_2 starts from scratch, re-installs the deps, re-loads the model, retries the hypothesis. With isorun’s fork primitive, the second hypothesis starts from where the first one diverged — at no extra cost beyond ~16 ms.
For an evaluation pipeline running 100 hypotheses against a 30-second setup, that’s the difference between 50 minutes and 60 seconds.