# 2. Serverless tool calls

Source: https://archil.com/guides/building-agents/serverless-tool-calls · Part of How to build agents · Facts checked 2026-11-05

**The short answer:** A serverless tool call runs one of the model’s commands on fresh, isolated compute with the agent’s workspace already mounted, and leaves nothing running. It should let the harness decide per call what the command can reach and change, commit its writes before it returns, and bill for the call instead of a machine-hour. On Archil, that call is `disk.exec()`.

A serverless tool call runs one of the model’s commands on compute that exists only for that command, not on a sandbox that’s up all session. [What comes after sandboxes](https://x.com/jhleath/status/2110407137616928811) describes the alternative: “the harness itself doesn’t actually care how or where the tools are run. It just needs to fire off a function call to something that then runs deterministic code and returns a result.” (From Hunter Leath’s post of October 14, 2026. Why the harness belongs outside the sandbox is the subject of [Two places to run an agent](https://archil.com/guides/building-agents/two-places-to-run-an-agent).)

Whatever runs that call has to provide six things:

- Isolation for each call, with nothing left running after it
- The workspace, mounted before the command starts
- Access to only what the call needs
- Writes that the loop and the next call can see
- Limits you can design around
- A price and an overhead that follow the work

Each section takes one and shows how Archil’s serverless execution provides it, starting from one function against the agent’s disk:

```typescript
import * as archil from "disk";

const disk = await archil.getDisk("dsk-0123456789abcdef");
const { exitCode, stdout, stderr, timing } = await disk.exec("npm test");
```

## Isolation for each call

Every call runs code nobody reviewed, so it gets compute of its own that’s gone when the command returns. On Archil, `disk.exec()` sends the command to Archil, not to a machine you manage. Archil starts a Firecracker microVM, with its own kernel, next to the disk’s data, mounts the disk, and runs the command with the disk as its working directory. When the command exits, the microVM unmounts the disk, which commits the command’s writes and hands back anything it checked out. Then the result comes back to your harness, and the microVM is gone. (From [Serverless execution](https://docs.archil.com/compute/serverless-execution) and the [TypeScript SDK](https://docs.archil.com/sdks/typescript#executing-commands). That the unmount commits and checks in before the result returns is from Archil’s container runtime.)

**One tool call, start to finish**

One serverless tool call. The harness calls `disk.exec("npm test")`. A fresh Firecracker microVM starts for the call, the workspace disk is mounted as its working directory, and `npm test` runs. When it exits with code 0, the disk is unmounted, which commits the writes and checks in anything the call checked out. The result, carrying stdout, the exit code, and timing, travels back to the harness, and the microVM is gone. The disk keeps every file. Under the drawing, the result’s timing fields: `queueMs` covers queueing, scheduling, booting the microVM, and mounting the disk; `executeMs` covers the command; and `totalMs` is the whole request, measured on the server.

Because the compute is new for every call, the workspace is the only thing that carries over. On Archil, files written to the disk persist, including installed packages and a virtual environment kept inside the workspace. Running processes, memory, environment variables, and files written anywhere but the disk don’t. Two habits follow. Install dependencies into the workspace once and reuse them, and chain related steps in one command, such as `cd app && npm run build && npm test`, instead of relying on an earlier call’s `cd` or `export`. (From [How to run agent tool calls without keeping a VM open](https://archil.com/article/how-to-run-agent-tool-calls-without-keeping-a-vm-open).)

## The workspace, mounted

The command has to find the repository, its packages, and the agent’s earlier changes at a path the model can rely on. With Archil, a harness that serves many tenants holds one client and mounts each tenant’s disk under its own ID, the form [Many agents, one machine](https://archil.com/guides/building-agents/many-agents-one-machine) wires into pi:

```typescript
import { Archil, ArchilApiError } from "disk";

const archil = new Archil({ apiKey: process.env.ARCHIL_API_KEY, region: "aws-us-east-1" });

// The bash tool. Each call gets a fresh microVM with the tenant's workspace at /mnt/archil/<diskId>.
export async function bash(diskId: string, command: string) {
  try {
    const { exitCode, stdout, stderr } = await archil.exec({
      disks: { [diskId]: { disk: diskId, subdirectory: "workspace", conditional: true } },
      command: `cd /mnt/archil/${diskId} && ${command}`,
    });
    return { exitCode, stdout, stderr };
  } catch (e) {
    if (e instanceof ArchilApiError && e.status === 504) {
      // The command ran past the per-call limit and was stopped.
      return { exitCode: 124, stdout: "", stderr: "Timed out. Split the work into smaller commands." };
    }
    throw e;
  }
}
```

Mounting only `workspace/` keeps the rest of the tenant’s disk, such as the conversation history, out of the command’s reach. `conditional: true` lets the command write without a checkout, for reasons later in this chapter. Tell the model the rules in its instructions: each command starts fresh in the workspace, and anything worth keeping belongs on the disk. The SDK retries an exec only when it couldn’t connect at all, so a retry never runs a command twice. (From the `disk` package’s retry policy and its [changelog](https://www.npmjs.com/package/disk) for version 1.6.0: creations, execs, and deletes retry only when no connection could be established.)

## Access to only what the call needs

The harness, not the model, should decide what each call can reach and what it can change. On Archil, `disk.exec()` mounts one disk at its root. `archil.exec()` takes a map instead, and mounts each disk at `/mnt/archil/<key>`. A common shape is an agent that reads a customer’s documents and writes to its own session workspace:

```typescript
const result = await archil.exec({
  disks: {
    workspace: { disk: sessionDiskId, conditional: true },
    docs: { disk: customerDocsDiskId, readOnly: true },
    team: { disk: teamDiskId, subdirectory: "users/ana", readOnly: true },
  },
  command: "grep -ril refund /mnt/archil/docs /mnt/archil/team > /mnt/archil/workspace/refunds.txt",
});
```

Each entry is either a disk ID, which mounts the disk’s root, or an object with these options:

- **`readOnly: true`** mounts the disk read-only. Every write from inside the microVM fails with `EROFS`, so a command can’t damage files that other threads or tenants read.
- **`subdirectory`** exposes only part of the disk. The path is relative, with no `.` or `..` segments, and the rest of the disk isn’t in the microVM at all.
- **`conditional: true`** mounts in conditional mode, which the next section explains.
- **`checkoutPaths`** and **`queueMs`** check paths out before the command starts, also explained in the next section. (The options are the `ExecMountSpec` type in the `disk` package and the `ExecMount` schema in our API. [Serverless execution](https://docs.archil.com/compute/serverless-execution#specifying-disk-options) documents `readOnly` and `subdirectory`; [Sharing disks](https://docs.archil.com/concepts/sharing-disks#selecting-a-mode-on-serverless-execution) documents `conditional`.)

A disk that isn’t in the map doesn’t exist inside the microVM. The request carries only the command and its mounts, so your API key stays in the harness.

**Compose a call**

An explorable with four disks: a session workspace (this thread’s repository and notes), customer documents (the tenant’s files, shared by every thread), a team drive with one folder per user, and app logs from every service. Each can be off, read-only, shared, or conditional, and the team drive and the logs can expose only `users/ana` or `app/logs`. It starts with the workspace conditional and the documents read-only. The figure lists what the command sees under `/mnt/archil/`. A read-only mount can change nothing, and every write fails with `EROFS`. A shared mount can create files and directories and edit files it opens, but deleting or renaming needs `archil checkout` on the parent. A conditional mount can change anything nobody else has checked out, and the last write to reach the disk wins. A subdirectory mount hides the rest of the disk. Disks that are off are out of reach. The figure also writes the matching `archil.exec` call, and says that a call needs at least one disk.

## Writes the loop can see

When a call returns, the harness and the next call have to see everything it wrote. When several writers share a workspace, you need to know whose write wins. Archil’s serverless execution has the same read-after-write consistency as any other Archil client. A file one command writes is visible to the next command that reads it, and a call’s writes are committed before its result returns. If committing fails, the exit code is non-zero and stderr says `archil: command output may not have been durably committed`, so a harness that checks `exitCode` already catches it. (From [Consistency with other clients](https://docs.archil.com/compute/serverless-execution#consistency-with-other-clients) and [Limits](https://docs.archil.com/compute/serverless-execution#limits) in our docs.)

How a call may write depends on the mount mode. By default, serverless execution mounts each disk in shared mode, which lets many clients mount one disk at once. In shared mode, the disk reads as read-only except where this client holds a checkout. Creating a file or a directory, or opening a file to write, checks it out automatically. Deleting or renaming is a change to the parent directory, so it needs a checkout of the parent, and without one it fails with `Read-only file system`. That surprises agents, which create a file in one call and can’t remove it in the next. (From [Automatic delegations in shared mode](https://docs.archil.com/concepts/sharing-disks#automatic-delegations-in-shared-mode). Our docs call a checkout a delegation.)

You have three ways to give a call the writes it needs:

- **Check out inside the command.** Start the command with `archil checkout .` to hold the whole disk for the call, as exclusive mode would. Writes then run at local-disk speed.
- **Check out before the command starts.** Pass `checkoutPaths: ["src"]` in the mount, and add `queueMs` to wait that many milliseconds for each checkout if another client holds it. `queueMs` alone checks out the mount’s root.
- **Mount in conditional mode.** With `conditional: true`, the call writes anywhere nobody else has checked out, with no checkout at all. Changes run on the server, at the speed of a remote file system, and if two clients write the same part of a file, the last write to reach the disk wins.

Checkouts last only as long as the call. We recommend conditional mode for an agent’s general-purpose bash tool. For one call that writes heavily to paths no other call touches, such as a dependency install, a checkout trades sharing for local speed. [Can many machines use it at once?](https://archil.com/guides/evaluating-storage/can-many-machines-use-it-at-once) compares the modes in depth.

**Three commands, three mounts**

The same three commands run in three calls against one disk: `echo ok > new.txt`, `echo '- ship' >> plan.md`, and `rm old.log`. In shared mode, the default, the create and the append are committed, and `rm old.log` fails with `Read-only file system`, because a delete needs a checkout of the parent directory. In shared mode with `archil checkout .` at the start of the command, all three are committed. With `conditional: true`, all three are committed without a checkout.

## Many calls at once

Each call runs in its own microVM, so `Promise.all` over a list of calls is a fan-out with no pool to manage. A harness can lint, type-check, and run test shards at once against the agent’s latest changes:

```typescript
const shards = [1, 2, 3, 4];

// The bash tool's mount, so every call writes without a checkout.
// Each call writes its own output file, so no two calls write the same path.
const runs = await Promise.all(
  shards.map((n) =>
    archil.exec({
      disks: { [diskId]: { disk: diskId, subdirectory: "workspace", conditional: true } },
      command: `cd /mnt/archil/${diskId} && mkdir -p results && npx vitest run --shard=${n}/4 > results/${n}.txt 2>&1`,
    }),
  ),
);
const failed = runs.filter((r) => r.exitCode !== 0).length;
```

The next call reads `results/`, and your harness reads `workspace/results/` from the same disk. Branches that only read should mount `readOnly: true`, so no branch can damage the files the others are reading. Branches that must not see each other’s writes at all need separate copies, which is what [checkpoints and branches](https://archil.com/guides/evaluating-storage/can-you-fork-it) are for. (From [How to fan out agent tool calls in parallel against one workspace](https://archil.com/article/how-to-fan-out-agent-tool-calls-in-parallel-against-one-workspace).)

**A fan-out against one workspace**

The harness starts its tool calls at once with `Promise.all`: lint, a type check, and four test shards. Each call gets its own microVM with the same workspace disk mounted, runs its command, writes its output to its own file under `results/`, and exits as soon as its command finishes. The microVMs finish at different times. All the results return to the harness, and the output files stay on the disk for the next call. Nothing is left running.

Parallel calls cost little extra time. In our measurement, calls in flight together took no longer than calls made one at a time, until the largest burst, which added about 160 ms at the median. Each account can run hundreds of commands in parallel, and we raise that to thousands on request. (Measured from a laptop to AWS `us-west-2`; see [How to fan out agent tool calls in parallel](https://archil.com/article/how-to-fan-out-agent-tool-calls-in-parallel-against-one-workspace). The parallelism limit is in [Serverless execution](https://docs.archil.com/compute/serverless-execution#limits).)

To search many files, don’t write the fan-out yourself. `disk.grep()` splits the listing and the matching across many microVMs and returns structured matches, and the whole search counts as one call however many workers it uses. (From [Search files](https://docs.archil.com/compute/search-files) and the [TypeScript SDK](https://docs.archil.com/sdks/typescript#searching-files).)

## Limits

Per-call compute trades a machine’s open-endedness for caps on each call, and the caps decide which steps still need a sandbox. Archil’s caps are in the following table, with what to do at each one.

| Limit                  | Value                                                       | What to do                                                                                                                                                                                                             |
| ---------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Command duration       | 5 minutes, then `504` with `Exec timed out`                 | Split long work, or run it in a [persistent sandbox](https://docs.archil.com/compute/sandboxes/introduction). Writes flushed with `fsync` before the timeout are durable; the rest are flushed on a best-effort basis. |
| Compute per command    | 2 vCPU and 4 GiB of memory, with no per-call sizing         | Move heavier steps to a persistent sandbox with the same disk mounted.                                                                                                                                                 |
| Options per call       | No timeout, abort signal, or environment variables          | Put variables in the command. A call the user cancels runs until it finishes or is stopped at the duration cap.                                                                                                        |
| Command length         | 100 KiB                                                     | Write long scripts to the disk and run the file.                                                                                                                                                                       |
| Output in the response | The trailing 128 KiB of stdout and of stderr                | Redirect large output to a file, and read the file back.                                                                                                                                                               |
| State between calls    | Only the disk                                               | Keep dev servers, watchers, and loaded interpreters in a persistent sandbox.                                                                                                                                           |
| Regions                | `aws-us-east-1`, `aws-us-west-2`, `aws-eu-west-1`, and BYOC | Create disks that run commands in an AWS region. A disk in `gcp-us-central1` stores data, but `exec` returns `Exec is not enabled`.                                                                                    |

Every microVM also starts with an empty client cache, so a command that walks thousands of files reads them fresh in every call. In our test, a second `rg` over the repository in the same microVM took about 0.12 s, under a third of its time in a fresh one. That gap is the reason to use `disk.grep()` for wide searches. (The limits are from [Limits](https://docs.archil.com/reference/limits#serverless-execution), [Serverless execution](https://docs.archil.com/compute/serverless-execution#limits), and [Region availability](https://docs.archil.com/reference/regions#compute-availability). BYOC deployments run serverless execution on other clouds and on bare metal; see [BYOC](https://docs.archil.com/reference/byoc). The cache measurement is from [How fast is a serverless tool call for an AI agent?](https://archil.com/article/how-fast-is-a-serverless-tool-call-for-an-ai-agent)) The steps that don’t fit, and how to move an agent that depends on them, are in [Migrating from an in-sandbox agent](https://archil.com/guides/building-agents/migrating-from-an-in-sandbox-agent).

## Price and speed

Per-call compute pays off only when the price follows the calls and the overhead is small next to a model turn. On Archil, each `disk.exec()` or `archil.exec()` invocation is one serverless tool call, even when it chains several commands, at $0.05 per 1,000 calls on the Developer and Team plans. The call’s CPU and memory are included, and the Free plan includes 1,000 calls a month. For scale, a persistent sandbox with the same CPU and memory as a call costs $0.19 an hour at our sandbox rates, or about $0.00005 a second. One call costs about what one second of that sandbox does, however long the command runs within its limit. (From [Pricing & Billing](https://docs.archil.com/administration/billing#usage-rates): $0.055 per vCPU-hour and $0.020 per GiB-hour for sandboxes. Storage and data transfer are metered separately, and model inference isn’t included.) What that means across a fleet of agents, idle time included, is the subject of [What it saves](https://archil.com/guides/building-agents/what-it-saves).

Speed is the other half. We timed calls against one disk holding the Express repository, from a laptop about 40 ms from the region, with 50 calls per row.

**How long a tool call takes**

Median end-to-end latency of serverless tool calls, measured from a laptop on the public internet to one disk in AWS `us-west-2`, 50 calls per row. The first part of each bar is the median `queueMs`. Quick commands, on a 0 to 300 ms scale: `true`, one at a time, 82 ms, with 37 ms of `queueMs`; `cat package.json` 92 ms (36); `ls -la lib` 84 ms (36); `true` ten at a time 81 ms (42); `true` fifty at once 245 ms (138). Real work, on a 0 to 5 s scale: `rg -n params .` over the repository 422 ms (35); `npm test`, with 1,288 tests, 4.7 s (36); `npm test` ten at a time 4.7 s (36); `npm test` fifty at once 4.8 s.

Read each bar as overhead plus work. Starting a microVM with the disk mounted took 35 to 40 ms when calls arrived at a normal pace, and a harness in the same region skips the laptop’s round trip. Our engineering team’s coding agents spent a median of 2.7 s on each model turn, so tens of milliseconds per tool call barely move how long a task takes.

Two cases are slower. The first call against a newly created disk took 331 ms at the median across five new disks. A call after the disk sat untouched for 11 to 20 minutes took between 0.25 s and 1.1 s, with `queueMs` of 74 to 219 ms, and most of the rest was the client opening a new connection. (All timings are from [How fast is a serverless tool call for an AI agent?](https://archil.com/article/how-fast-is-a-serverless-tool-call-for-an-ai-agent), which includes a script to reproduce them. The model-turn median is from 30 days of our team’s agent transcripts, described in [Do AI agents need a sandbox running for the whole session?](https://archil.com/article/do-ai-agents-need-a-sandbox-running-for-the-whole-session))

Your numbers depend on your region and your harness’s distance from it. Every result carries `timing.queueMs` and `timing.executeMs`, so log both with each tool call and measure the overhead and the work where your agents run. [Many agents, one machine](https://archil.com/guides/building-agents/many-agents-one-machine) puts this call behind a harness that serves every tenant from one process.
