Back to articles

How to fan out agent tool calls in parallel against one workspace

Keep the workspace on one disk and run each branch as its own exec call in its own microVM: read-only and conditional mounts, measured latency at 10 and 50 calls, and limits.

5 min read

To fan out an agent's tool calls in parallel against one workspace, keep the workspace on a shared disk and run each branch as its own short-lived command against that disk, then wait for all of them together. With Archil, each exec call gets its own Firecracker microVM with the disk mounted, so ten or fifty calls run at the same time without a pool of sandboxes to manage, and every branch sees the same files.

How to fan out agent tool calls in parallel against one workspace

Agents fan out more than they used to: run several test files at once, try three fixes side by side, lint and type-check and test in parallel, or let subagents work on different parts of one repository. Each branch needs a machine to run on and the same working directory.

With one long-lived sandbox, branches share its CPUs and compete for them. With one sandbox per branch, you copy or sync the workspace into each one and gather the results back. Serverless tool calls avoid both: the workspace stays on one disk, and each branch is one call that starts a microVM, mounts the disk, runs its command and exits.

Example: run test files in parallel against one repo

This runs five test files from the same checkout at once. Each call writes its output to its own file, so the branches never write to the same path.

TypeScript
import * as archil from "disk";
 
const repo = process.env.ARCHIL_DISK_ID!;
const files = ["test/app.router.js", "test/app.route.js", "test/res.send.js", "test/req.query.js", "test/app.param.js"];
 
// Each archil.exec call runs in its own microVM. Conditional mode lets every branch
// write to the shared disk without a checkout; each writes its own output file.
const runs = await Promise.all(
  files.map((file, i) =>
    archil.exec({
      disks: { work: { disk: repo, conditional: true } },
      command: `cd /mnt/archil/work/express && mkdir -p ../results && npx mocha --require test/support/env ${file} > ../results/${i}.txt 2>&1; echo $?`,
    }),
  ),
);
 
console.log(runs.map((r) => r.stdout.trim())); // exit code of each branch

The next call, from the agent or from your app, reads all five results from results/ on the same disk. Your application can also read them without starting anything, through the SDK's getObject or the S3-compatible API.

Read-only branches and writing branches

How you mount the disk in each call decides what the branches can do to each other:

  • Read-only branches. For searches, analysis or tests that only read, pass readOnly: true. Code in the microVM can't modify the disk, so no branch can damage the workspace the others are reading.
  • Writing branches. Pass conditional: true. In conditional mode, any client can write to any path another client hasn't checked out, without calling archil checkout first. Clients don't lock files for each other, so if two branches write the same part of the same file, the last write to reach the disk wins. Give each branch its own output path, as the example does.
  • Default mode. Without either option, serverless execution mounts the disk in shared mode, where a client takes a write lock on files and directories it creates. That works for branches that write to separate places, but conditional mode is simpler to reason about when many branches write at once.

When a call returns, its writes to the disk have been committed, so the next call sees them. See Sharing disks for how the modes work.

How fast parallel calls are

We measured fan-out against one disk from a laptop to AWS us-west-2, end to end including the internet round trip:

Calls in flightCommandp50p90
1 at a timetrue82 ms95 ms
10 at a timetrue81 ms115 ms
50 at oncetrue245 ms292 ms
1 at a timeExpress test suite (1,288 tests)4.7 s5.8 s
10 at a timeExpress test suite4.7 s6.3 s
50 at onceExpress test suite4.8 s6.2 s

Ten calls in flight cost about the same per call as one. Fifty started in the same instant added about 160 ms at the median, which is small next to any command that does real work: fifty copies of the test suite finished in about the time one takes. Every result reports timing.queueMs and timing.executeMs, so you can watch this in your own agent.

Each account can run hundreds of serverless commands in parallel, and Archil can raise the limit to thousands on request.

When fan-out isn't the right tool

  • Searching a whole disk. To find matching lines across many files, use disk.grep instead of one exec per directory. It splits the listing and matching across many microVMs for you and returns the matches.
  • Branches that need a long-running process. A branch that starts a server and then queries it has to do both inside one command, within the 5 minute limit per call. If the server has to outlive the call, use a persistent sandbox.
  • Branches that read a lot of files. Each microVM starts with an empty client cache, so a command that walks many files pays to read them in every branch. In our test, the first rg over the Express repository in a fresh microVM took about 0.4 s, and running it again inside the same microVM took about 0.12 s. That is fine for a handful of branches; for wide searches, disk.grep is the better fit.
  • Branches that must not see each other's writes. Branches share one disk. To give each attempt its own independent copy of the workspace, use checkpoints and branches and point each attempt at its own branch.

Each exec call counts as one serverless tool call at $0.05 per 1,000 calls, with execution included, so fifty parallel calls cost the same as fifty sequential ones. See Pricing & Billing.

Hunter Leath

Founder & CEO