Skip to content

Blog

Agent sandbox or serverless functionwhen each one fits

Use a serverless function for short, trusted steps that you wrote. Use a sandbox for code a model wrote that needs a shell, files and time.

· The Runtime team

9 min read

Read as Markdown

Runtime (withruntime.com) gives each conversation its own Firecracker microVM. It pauses itself after 300 seconds with nothing happening, and wakes for the next command. Most agent products need both kinds of compute. The mistakes come from putting a part in the wrong one. This post splits a typical agent product into its parts. It says where each part belongs and why, and it shows the twenty lines that connect a function to a sandbox safely.

What is the real difference between a function and a sandbox?

A function is an invocation that you pass an event to. A sandbox is a machine that you keep for as long as the work needs it. The rest of this table follows from that difference:

Question Serverless function Agent sandbox
What you deploy Your code, reviewed and built ahead of time Nothing; code arrives while it runs
Whose code runs Yours The model's, and whatever it installs
State between calls Not promised; reload it each time Files, memory and processes stay
What it can reach by default Your cloud account, through the function's role The internet, or nothing, as you set it
How you talk to it One request, one response Many commands, files and processes over a session
How long one piece of work may take Capped by the platform, in minutes As long as it works, while credit lasts
Who it serves Every user, on shared warm instances One user or one task, then gone

The two rows that matter most for agents are "whose code runs" and "what it can reach". A function is built to run trusted code with your credentials. A sandbox is built to run untrusted code with none.

Why not run model-written code inside the function?

Because the function already holds your keys. A function runs with an identity: an AWS role, a Google service account, or the environment variables your platform injects. If you exec code that a model wrote inside that function, the code inherits all of it. One prompt injection is enough. Then env or the metadata endpoint hands an attacker whatever the function can do, which is usually read and write access to your own data (how injected text becomes a command).

Three more reasons appear in practice:

  • Warm instances are reused. Platforms keep an instance warm and send it the next request. A file that one user's code wrote to /tmp can still be there when the next user's code runs.
  • The file system is mostly read-only. pip install at run time either fails or writes to a scratch folder that is deleted later. The model cannot add the library it needs.
  • The run is capped. A test suite, a build or a data job that runs past the platform's limit is stopped, with no way to resume.

A sandbox reverses each of these. It is a fresh machine with its own kernel. It holds nothing from any other request. It lets you use sudo to install anything. It has no credentials unless you add them (run untrusted LLM code).

Which parts of an agent product belong where?

Split the product into four parts, and the answer is usually clear:

  1. The front door: webhooks from GitHub or Slack, your API, the chat endpoint. Short, stateless, your own code, called by many users. A function fits.
  2. The agent loop: call the model, read the tool calls, run them, repeat. It is your code, but it spends most of its time waiting on the model. Either one fits, depending on how long a task runs (next section).
  3. Tool execution: the shell commands, Python, tests and installs the model asks for. Untrusted, stateful, sometimes long. A sandbox.
  4. The record: writing results, transcripts and usage to your database. Trusted, short. A function, or the end of the loop.

The common mistake is to merge 2 and 3 into one function. That gives the model's code your loop's credentials. The other mistake is to merge 1 and 3 into one sandbox per request. That pays to start a machine for work that needed none.

Can a function run the agent loop?

Yes, if one task fits inside the platform's time limit. You pay for the waiting, though. A loop with ten model calls of twenty seconds each spends over three minutes waiting and only seconds working. A function billed on wall-clock time charges for its full memory during all of that wait. A durable workflow engine can split the loop into steps to stay under the cap.

For work that runs longer than the cap, or that you want to survive a redeploy, run the loop inside the sandbox. Store the model's API key as a secret. The sandbox can use the secret, but it cannot read it (secrets). Your function starts the task and returns. A webhook tells you when the task is done (background agents). The sandbox bills CPU only while the loop actually computes. The waiting costs $0.03125 an hour for 2 vCPU and 4 GiB.

How do you call a sandbox from a function safely?

Name the sandbox after the conversation, and use getOrCreate. A retry then finds the machine that the first try made. Function platforms retry failed invocations. A webhook sender retries when you answer slowly. A handler that calls a plain create makes a second sandbox on each retry. With a name, the handler is safe to run twice. The conversation's files, installed packages and running processes also stay in place for the next turn:

TypeScriptimport { Sandbox } from "withruntime";type Turn = { conversationId: string; code: string };// Your function's handler: one turn of one conversation.export async function handler({ conversationId, code }: Turn) {  const sbx = await Sandbox.getOrCreate(`chat-${conversationId}`, {    labels: { app: "chat" },    network: { internet: false }, // the model's code reaches nothing    idlePauseSeconds: 120,  });  await sbx.files.write("/workspace/turn.py", code);  const run = await sbx.exec(["python3", "turn.py"], { timeoutMs: 60_000 });  return {    ok: run.exitCode === 0,    output: (run.stdout + run.stderr).slice(-8000),    timedOut: run.timedOut,  };}console.log(await handler({ conversationId: "c-42", code: "print(sum(range(10)))" }));
Pythonfrom withruntime import Sandboxdef handler(conversation_id: str, code: str) -> dict:    """Your function's handler: one turn of one conversation."""    sbx = Sandbox.get_or_create(        f"chat-{conversation_id}",        labels={"app": "chat"},        network={"internet": False},  # the model's code reaches nothing        idle_pause_seconds=120,    )    sbx.files.write("/workspace/turn.py", code)    run = sbx.exec(["python3", "turn.py"], timeout_ms=60_000)    return {        "ok": run.exit_code == 0,        "output": (run.stdout + run.stderr)[-8000:],        "timed_out": run.timed_out,    }print(handler("c-42", "print(sum(range(10)))"))

This gives you the following, with no state kept in the function:

  • Nothing to store. The name is the handle. The function needs no table that maps conversations to sandbox ids (find a sandbox by name).
  • Nothing to wake. Between turns, the sandbox pauses itself. The next exec wakes it, with its files and memory as they were (wake on request).
  • Nothing to clean up per request. The handler never stops the sandbox. A nightly job calls runtime.sandboxes.stopAll({ labels: { app: "chat" } }), or stops only the sandboxes whose conversations have ended.

For a sandbox that must be new on each call, such as one per job rather than one per conversation, pass an idempotency key to create. Derive the key from the event's ID. A retried event then gets the same sandbox back, not a second one (retry safely):

TypeScriptimport { randomUUID } from "node:crypto";import { Runtime } from "withruntime";const runtime = new Runtime();// A queue or webhook handler: one sandbox per job, however often the event arrives.export async function onJob(event: { id: string; command: string }) {  const sbx = await runtime.sandboxes.create(    { labels: { app: "jobs", event: event.id }, timeoutSeconds: 1800 },    { idempotencyKey: `job-${event.id}` },  );  try {    const run = await sbx.exec(event.command, { timeoutMs: 600_000 });    return { exitCode: run.exitCode, tail: run.stdout.slice(-2000) };  } finally {    await sbx.stop();  }}// The event's id comes from your queue; it is the same every time the event is redelivered.const event = { id: `evt_${randomUUID()}`, command: "python3 -c 'import sys; print(sys.version)'" };console.log(await onJob(event));
Pythonfrom uuid import uuid4from withruntime import Runtimeruntime = Runtime()def on_job(event_id: str, command: str) -> dict:    """A queue or webhook handler: one sandbox per job, however often the event arrives."""    sbx = runtime.sandboxes.create(labels={"app": "jobs", "event": event_id},                                   timeout_seconds=1800, idempotency_key=f"job-{event_id}")    try:        run = sbx.exec(command, timeout_ms=600_000)        return {"exit_code": run.exit_code, "tail": run.stdout[-2000:]}    finally:        sbx.stop()# The event's id comes from your queue; it is the same every time the event is redelivered.print(on_job(f"evt_{uuid4()}", "python3 -c 'import sys; print(sys.version)'"))

How fast is the call from a function?

A command to a running sandbox is one HTTPS request to the API, plus the network time between your function's region and Runtime's. The speed guide lists an operation's time once it is within 2× of its floor, measured each minute from outside.

You do not need a pool of warm sandboxes: Runtime keeps spares of the common shapes started (warm pool). The first turn of a conversation can create its sandbox inside the user's request (what a cold start is).

What does each one cost for agent work?

Each one bills for something different, so the cheaper one depends on how much of the time is spent waiting. A function bills the memory it was given for every millisecond of the invocation. A Runtime sandbox bills memory while it runs. It bills CPU only for what its commands use, at $0.025 per vCPU-hour. While paused, it bills only storage (pricing).

Take a chat product with 1,000 conversations a day and 15 turns in each conversation. The model's code runs 2 CPU-seconds per turn. The sandbox stays awake for two minutes after each turn before it pauses:

Awake
1,000 × 15 turns × 2 min / 60 × $0.03125
$15.63 a day
CPU
1,000 × 15 × 2 s / 3,600 × $0.025
$0.21 a day
Total
$15.83 a day

Most of that cost is the awake window, so that is the lever. Drop idlePauseSeconds to 30, and the awake line falls to $3.91. The cost is a wake on any turn that arrives later than that (pause between turns). A function can run the same code only if the code is your own. So the comparison that matters is where the untrusted part runs. In a function, it runs beside your credentials (Lambda against a sandbox, priced).

When is a function the better choice?

When the code is yours, the work is short and stateless, and it needs your cloud account's identity:

  • Glue between services parse a webhook, call an API, write a row.
  • High fan-out over your own code resize ten thousand images with a library you deployed.
  • Anything that must run inside your own cloud account for compliance reasons.

Runtime does not host your functions. It is where the model's code runs. Most teams end up with exactly that split. Their front door and record-keeping stay on the function platform they already use, and every tool call runs in a sandbox.

In short

  • A function runs your code with your credentials. A sandbox runs the model's code with none. Match each piece of code to the right kind of compute.
  • Keep the agent loop in a function only if a task fits the time limit; otherwise run it in the sandbox with its key as a secret it cannot read.
  • Call sandboxes from functions with getOrCreate and a stable name, so retries are safe and every turn finds its machine.
  • Most of a sandbox's cost is the time it stays awake; tune idlePauseSeconds to your turn length.

Run it on Runtime

For an agent that mostly waits on a model, Runtime costs 42% to 88% less than fourteen other sandbox providers (compare costs). A Runtime sandbox answers over HTTPS from any function platform. Read the quick start and call one from your existing function today.

Keep reading.

Cut your cloud billby 75%.

No card to start. Billed by the second after.

Start free, no card