The best sandbox APIs for Rust AI agents in 2026
If you are building an agent in Rust, you have already noticed that nobody writes for you. Every sandbox platform's quickstart is Python, the second tab is TypeScript, and the REST reference is where you actually live. That is fine — a well-designed HTTP API is a perfectly good SDK, and Rust's ecosystem gives you a better client than most vendors ship anyway.
But it does change what you should evaluate. When you are writing the client, the vendor's SDK ergonomics stop mattering and the shape of the HTTP surface starts mattering enormously.
Ground rules, because they matter more in a vendor's post than anywhere else. PandaStack is mine and appears below. Specific numbers appear only for my own system, where I can point at the code that produces them. And I will not tell you whether any vendor ships an official Rust crate, because that is exactly the sort of fact that changes between me writing this and you reading it — check crates.io and the vendor's GitHub org yourself, and note the date you checked.
Why this is not optional
Rust makes it tempting to skip. Your process is memory-safe, your dependencies are audited, you feel like the careful one. None of that helps: the moment your agent calls std::process::Command with a string a language model produced, the safety guarantees of your language stop being relevant. The subprocess has your environment variables, your filesystem, your network, and your cloud credentials.
The other reason is more mundane. Model-written code hangs, allocates until the machine swaps, and leaves files behind. In your own process each of those is an incident. Somewhere else, each of them is a sandbox you delete.
The checklist, from a Rust caller's point of view
- Does cancellation propagate? Rust futures are cancelled by dropping them, so your agent will drop an exec request the moment the user hits stop. If the API has no way to signal that server-side, you have just orphaned a running command that will burn CPU until its timeout. Look for an explicit kill or delete endpoint you can call from a Drop impl or a cancellation branch.
- Are timeouts a server-side parameter or only a client-side deadline? A client timeout abandons the request; a server-side timeout actually stops the work. You want the second one, and you want to know what the server does when it fires.
- Is streaming available and in what format? Server-sent events map cleanly onto a reqwest byte stream. A bespoke framing protocol means you are writing a parser. WebSockets mean pulling in another dependency and its runtime assumptions.
- Are errors typed, or is everything a 500 with prose? From Rust you want to match on a discriminant, not substring-search an error message. Ask specifically how a timeout, an out-of-memory kill, and a non-zero exit code are distinguished — those three get conflated constantly.
- What is the auth model? A static bearer token is trivially handled. Anything requiring a signing algorithm or a token refresh dance is real work when there is no SDK doing it for you.
- Is there an OpenAPI document? This is the single highest-leverage question for a Rust caller, because it means you can generate types instead of hand-writing them, and you find out about API changes at compile time.
- Can you self-host or export? Agent infrastructure has a habit of becoming load-bearing. Knowing the exit exists changes how much you are willing to build on it.
The field, described honestly
- E2B — the most established code-interpreter platform, built around a stateful kernel session that keeps variables and imports between calls. Rich result types including images and charts, which is what you want if the agent is doing data analysis. Evaluate the HTTP surface for the session lifecycle rather than the SDK, since you will be driving it directly.
- Modal — strongest when the workload is Python-shaped batch or inference and you want fan-out. Its model is defined functions rather than a generic machine, which is a poor fit if your agent needs an arbitrary shell and a good fit if it needs to run the same computation a thousand times.
- Daytona — developer-environment heritage, so the unit is a workspace rather than an execution. Right if your agent works in a repository over minutes rather than running a snippet.
- Runloop — aimed at coding agents specifically, with devbox-shaped environments and snapshots. Worth evaluating directly if repository work is the use case.
- Cloudflare Sandbox SDK — execution sandboxes inside the Workers platform. Sensible if the rest of your stack is there; less so as a standalone dependency for a Rust service running elsewhere.
- Fly Machines — not an agent product at all, but a fast VM API with a clean REST surface. If you are the sort of Rust developer who would rather own the orchestration than learn someone's abstraction, this gives you the raw material. Lifecycle, cleanup, and quotas become your code, and that is a real amount of code.
- PandaStack — Firecracker microVMs behind a REST API with an OpenAPI document, server-side exec timeouts, server-sent-event streaming for command output, and typed error responses. Create is a snapshot restore rather than a boot, which is why it lands around 179ms at the median rather than in seconds. Sandboxes can be forked copy-on-write, and the same API also runs managed Postgres, so an agent can be handed a real database alongside its machine. There is no official Rust crate — you will be generating a client from the OpenAPI document, which is the honest state of every platform on this list from where you are sitting.
A client that does not embarrass you
The shape below is the one worth copying regardless of vendor: typed errors, a server-side timeout passed through, and a guard type that cleans up the sandbox even when the agent loop is cancelled.
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, thiserror::Error)]
pub enum SandboxError {
#[error("command timed out after {0}s")]
Timeout(u64),
#[error("command exited with status {status}: {stderr}")]
NonZeroExit { status: i32, stderr: String },
#[error("transport: {0}")]
Transport(#[from] reqwest::Error),
}
#[derive(Serialize)]
struct ExecRequest<'a> {
cmd: &'a str,
timeout_seconds: u64,
}
#[derive(Deserialize)]
pub struct ExecResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
pub struct Sandbox {
http: reqwest::Client,
base: String,
token: String,
pub id: String,
}
impl Sandbox {
pub async fn create(base: &str, token: &str, template: &str) -> Result<Self, SandboxError> {
let http = reqwest::Client::builder()
// A generous client-side ceiling. The real limit is server-side,
// per-command, below — this only catches a wedged connection.
.timeout(Duration::from_secs(120))
.build()?;
#[derive(Deserialize)]
struct Created { id: String }
let created: Created = http
.post(format!("{base}/v1/sandboxes"))
.bearer_auth(token)
.json(&serde_json::json!({ "template": template, "ttl_seconds": 1800 }))
.send().await?
.error_for_status()?
.json().await?;
Ok(Self { http, base: base.into(), token: token.into(), id: created.id })
}
pub async fn exec(&self, cmd: &str, timeout: u64) -> Result<ExecResult, SandboxError> {
let res: ExecResult = self.http
.post(format!("{}/v1/sandboxes/{}/exec", self.base, self.id))
.bearer_auth(&self.token)
// Server-side timeout: the command is actually stopped, not just
// abandoned by the client. This is the distinction that matters.
.json(&ExecRequest { cmd, timeout_seconds: timeout })
.send().await?
.error_for_status()?
.json().await?;
if res.exit_code != 0 {
return Err(SandboxError::NonZeroExit {
status: res.exit_code,
stderr: res.stderr,
});
}
Ok(res)
}
pub async fn delete(&self) -> Result<(), SandboxError> {
self.http
.delete(format!("{}/v1/sandboxes/{}", self.base, self.id))
.bearer_auth(&self.token)
.send().await?
.error_for_status()?;
Ok(())
}
}The part everyone gets wrong: cleanup on cancellation
In Rust, cancelling an async operation means dropping the future. Your agent loop will do this — a user cancels, a timeout fires higher up, a select! branch wins. If the only cleanup you wrote is a delete call after the last await, none of it runs.
Drop cannot await, so the reliable pattern is to hand the deletion to a detached task. It is a few lines and it is the difference between a tidy fleet and a slow leak of sandboxes nobody can account for.
pub struct SandboxGuard {
inner: Option<std::sync::Arc<Sandbox>>,
}
impl SandboxGuard {
pub fn new(sb: Sandbox) -> Self {
Self { inner: Some(std::sync::Arc::new(sb)) }
}
pub fn get(&self) -> &Sandbox {
self.inner.as_ref().expect("guard used after drop")
}
}
impl Drop for SandboxGuard {
fn drop(&mut self) {
if let Some(sb) = self.inner.take() {
// Drop cannot await, so spawn the delete. Fire-and-forget is
// acceptable here precisely because the platform also enforces a
// TTL — belt and braces, and the TTL is the braces.
tokio::spawn(async move {
if let Err(e) = sb.delete().await {
tracing::warn!(sandbox = %sb.id, error = %e, "sandbox cleanup failed");
}
});
}
}
}
// Usage inside an agent loop. Whether this returns normally, returns an
// error, or is cancelled by a select! branch, the sandbox goes away.
async fn run_tool(base: &str, token: &str, code: &str) -> Result<String, SandboxError> {
let guard = SandboxGuard::new(Sandbox::create(base, token, "code-interpreter").await?);
let out = guard.get().exec(&format!("python3 -c {}", shell_quote(code)), 30).await?;
Ok(out.stdout)
}Pick by situation
- Agent runs self-contained snippets and returns results → a code-interpreter platform with a stateful kernel session. Prioritise result typing and session lifecycle in the HTTP API.
- Agent works in a repository over minutes → a workspace-shaped platform, and evaluate how long a warm workspace lives and what it costs while idle.
- You want to own the orchestration → a low-level VM API. Budget for lifecycle, quotas, and cleanup as real engineering work, not a weekend.
- The agent needs a database as well as a shell → a platform that provides both, or you are gluing two vendors together for every environment.
- Compliance requires a hard isolation boundary → microVMs or dedicated instances. A shared-kernel container is a different security claim and you should read it carefully.
- You want to generate your client → make the OpenAPI document a hard requirement in the evaluation. From Rust it is worth more than any SDK the vendor could have written.
The short version
Building an agent in Rust means you will write the sandbox client yourself, and that is genuinely fine — a few hundred lines of reqwest, serde, and thiserror gets you something better typed than most shipped SDKs. Evaluate the HTTP surface, not the vendor's Python quickstart.
The three things that will actually hurt you if you get them wrong are cancellation, server-side timeouts, and typed errors. Ask about those three in the first conversation, verify them yourself against a trial account, and the rest of the decision is ordinary shopping.
Frequently asked questions
Can I sandbox model-generated code inside my Rust process?
Not in any way that constitutes a security boundary. Rust's memory safety protects you from a class of bugs in your own code; it does nothing about a subprocess you deliberately spawn with attacker-influenced arguments, which inherits your environment variables, your filesystem access, and your network. WebAssembly runtimes give a real in-process boundary and are worth considering when the code is Wasm and needs no system access, but that constraint eliminates most of what agents actually want to do — install a package, read a file, call an API. Hand-rolled controls like a chroot plus a seccomp filter can work, and you have now taken on maintaining a security control indefinitely. For anything user-facing, a separate machine with its own kernel is the boundary, and the only real decision is whether you operate it or someone else does.
How do I handle cancellation when the agent is stopped mid-execution?
Two mechanisms, and you need both because they cover different failures. Client-side, remember that cancelling a Rust future means dropping it, so cleanup written after the final await simply does not run — put the sandbox in a guard type whose Drop implementation spawns a detached delete task, since Drop itself cannot await. Server-side, pass a timeout as a request parameter so the platform actually stops the command rather than your client merely abandoning the response, and set a TTL at creation so the sandbox expires even if your process is killed before any cleanup can run. The client path is the fast one that keeps your fleet tidy; the TTL is the one that saves you when a pod is evicted mid-loop.
Is there an official Rust SDK for code execution sandboxes?
Deliberately not answered here, because it is precisely the kind of fact that changes shortly after publication and getting it wrong is worse than saying nothing. Check crates.io and the vendor's GitHub organisation directly, look at the last commit date and the open issue count rather than the existence of a repository, and note the date you checked. What is generally true is that Python and TypeScript are first-class everywhere and other languages are served by the REST API, so plan for writing a client. The question that matters more than SDK availability is whether the platform publishes an OpenAPI document, because that turns client-writing into code generation and gives you compile-time notice when the API changes.
What should I look for in a sandbox REST API from Rust specifically?
Four things, in order of how much pain they cause when missing. Server-side timeouts as a request parameter, so a hung command is actually stopped rather than merely abandoned by your client. An explicit kill or delete endpoint you can call from cleanup code, since Rust cancellation is a dropped future and you need something to call. Typed error discriminants so you can match rather than substring-search prose, particularly distinguishing a timeout from an out-of-memory kill from a non-zero exit — those three get conflated constantly and they need different retry behaviour. And an OpenAPI document, which lets you generate types instead of hand-writing them. Streaming over server-sent events is a strong bonus, because it maps directly onto a reqwest byte stream without adding a WebSocket dependency.
How fast does sandbox creation need to be for an interactive agent?
It depends on whether the user is waiting, and the honest answer is that most teams over-optimise this. If a person is watching a response stream, anything under about a second disappears into the surrounding model latency, which is usually seconds anyway. If you create a sandbox per tool call in a loop, the number compounds and starts to matter. Platforms that restore from a snapshot rather than booting a machine are in the hundreds-of-milliseconds range, while image-pull-based approaches are usually seconds. Before optimising, though, check the architecture: reusing one sandbox across an agent session, with a stateful kernel, removes the create cost from every call after the first, and that is a larger win than any difference between vendors.
Keep reading
49ms p50 cold start. Fork, snapshot, and scale to zero.