A source-level teardown of 16 sandbox and compute SDKs, and the design decisions that came out of it: method-aware retries, typed errors with metadata, zero runtime dependencies.
Before we wrote a line of the createos-sandbox SDK, we cloned 16 competitors and read their source. Not the docs. The source. E2B, Daytona, Modal, Cloudflare, CodeSandbox, Vercel, Morph, Islo, Blaxel, Superserve, Alibaba's OpenSandbox, Beam, Tensorlake, and a few more.
The survey turned into an 880-line internal doc. The decisions that shape an SDK live in its transport layer, not its README. What we copied, what we threw out, and why:
The parts everyone gets right
A few patterns showed up in almost every SDK, and they earned their place.
A stateful handle returned from a factory. You call createSandbox() and get back an object that knows its own ID and lets you act on it. E2B, Daytona, Modal, and CodeSandbox all land here. We did too:
import { CreateosSandboxClient } from "@nodeops-createos/sandbox";
const client = new CreateosSandboxClient();
const sandbox = await client.createSandbox({ shape: "s-4vcpu-4gb", rootfs: "devbox:1" });
const { result } = await sandbox.runCommand("node", ["--version"]);
console.log(result.stdout); // v20.x.x
await sandbox.destroy();
One namespace, not five. Daytona nests .fs, .git, .process, .computerUse, .codeInterpreter off the handle. Cloudflare exposes a flat 40-method surface. ComputeSDK keeps everything flat except .filesystem, and that restraint reads better in an editor. We kept one .files namespace and left the rest flat on the handle. If the control plane has no git endpoint, the SDK has no .git.
Environment-variable config. CREATEOS_SANDBOX_API_KEY and CREATEOS_SANDBOX_BASE_URL, so a script needs zero setup to run. Standard, and every good SDK does it.
None of that sets us apart. The real gaps showed up one layer down, in how each SDK handles failure.
Retries, and why most SDKs get them wrong
A sandbox SDK talks to a microVM scheduler that sheds load with transient 503s, so it has to retry. Most don't:
- E2B: no retries at all.
-
Daytona: ships
axios-retryas a dependency, never wires it in. - ComputeSDK, Morph, Beam, OpenSandbox, OpenComputer: no retry layer.
- CodeSandbox: fixed-delay, no backoff, no jitter.
-
Cloudflare: retries
503only, no jitter, no network-error retry.
Vercel and Modal are the exceptions. Both do it well: exponential backoff, jitter, and they honor Retry-After. We matched that, then added one thing they don't split on cleanly: the retry policy is method-aware.
The question that decides whether a retry is safe: if the server already processed this request, does resending it cause harm? A repeated GET or DELETE is fine. A repeated POST might create a second sandbox or double-charge bandwidth. So idempotent and non-idempotent methods get different rules:
| Trigger | GET HEAD PUT DELETE |
POST PATCH |
|---|---|---|
| Network error (DNS, reset, refused) | Retried | Not retried |
500 / 502 / 504
|
Retried | Not retried |
503 Service Unavailable |
Retried | Retried |
429 Too Many Requests |
Retried | Retried |
A POST retries only on 429 and 503, the two statuses where the server provably rejected the request before touching the handler. Every other failure on a POST surfaces immediately as a typed error and lets your code decide. Streaming requests are never retried, because a half-consumed NDJSON stream has no safe replay position.
Backoff is min(base × 2^attempt + random·base, cap). The jitter term is the point. When a host restarts and 200 clients all see the same 503, pure exponential backoff makes them retry in synchronized waves. The random offset spreads them out.
Typed errors that teach
The second gap: error models. ComputeSDK, Morph, Beam, and OpenSandbox throw new Error("HTTP " + status) with a stringified body. To branch on the failure you parse the message string, which breaks the moment the server changes its wording.
E2B, Daytona, and Cloudflare do this right, and E2B's touch is the one I liked most: a 502 error message tells you it's likely a sandbox timeout, call .setTimeout. The error teaches you the fix.
We shipped a 10-class hierarchy you narrow with instanceof, no string parsing:
try {
await sandbox.runCommand("ls", ["/"]);
} catch (err) {
if (err instanceof CreateosSandboxNotFoundError) {
return null; // 404: already destroyed
}
if (err instanceof CreateosSandboxRateLimitError) {
// retries already exhausted by the SDK
await sleep((err.retryAfterSeconds ?? 5) * 1000);
}
throw err;
}
Every HTTP error carries structured fields instead of a prose blob: statusCode, method, endpoint, resourceId parsed out of the path, requestId, and a machine-readable code. Quote the requestId in a support ticket and the operator finds your exact call in O(1). Modal has a good retry engine but its errors carry no code or requestId, so you lose that thread.
The things we said no to
Copying good ideas is easy. The rejections took more argument. Three we turned down:
gRPC and protobuf (Modal). Modal has the best retry implementation in the survey, built on gRPC over HTTP/2. It also carries checked-in codegen, two ts-proto patch hacks, and heavy dependencies. For a REST + NDJSON control plane, that's enormous build complexity for no user-facing gain. We passed.
OpenAPI codegen (Vercel, Islo, Blaxel). Vercel's SDK is the strongest proof that codegen scales: 40-plus namespaces, one hand-edited file. But it pays off at 40 operations, not at 12. Blaxel's generated build needs 11 sed patches and a perl rewrite after every regeneration. Our surface is small enough to hand-write and keep readable.
WebSocket data plane (CodeSandbox, Daytona). Elegant for resume and reconnect. Our control plane is REST + NDJSON with no socket endpoint, so a WS layer would model a connection that doesn't exist.
The result is zero runtime dependencies and nine modules, each under 300 lines. Cloudflare's sandbox.ts is a single 6,295-line file. Ours is a hand-written fetch client you can read in an afternoon.
Why this matters when the caller is an agent
Half the traffic to a sandbox SDK now comes from an LLM writing code, not a human. That changes what a good SDK owes its caller.
An agent can't read a 502 and intuit "call .setTimeout." It works off types and structured signals. Typed errors with a stable code field give it something deterministic to branch on. Method-aware retries mean an agent that fires a burst of POSTs during a retry storm won't silently spawn duplicate VMs. And zero dependencies matter more, not less, when the SDK runs inside a sandbox that another agent spawned.
We also ship llms.txt and a bundled llms-full.txt, so a coding agent can load the entire API surface as context instead of guessing from method names.
Read it yourself
The SDK is @nodeops-createos/sandbox: TypeScript, zero runtime dependencies, ESM, runs on Node 20+, Bun, Deno, Cloudflare Workers, and the browser. Docs follow the Diátaxis framework, and the full 16-SDK comparison lives in the repo.
If you maintain a sandbox or compute SDK, clone a few competitors and read the transport layer. It's the fastest way to find out what your own SDK is missing.
Refs
Quickstart: https://nodeops.network/createos/docs/Sandbox/Quickstart
Sandbox web: https://createos.sh/app/sandbox
SDK: https://github.com/NodeOps-app/createos-sandbox-sdk
SDK Examples: https://github.com/NodeOps-app/createos-sandbox-sdk
CLI: https://github.com/NodeOps-app/createos-cli
Docs: https://nodeops.network/createos/docs/Sandbox/Overview
Run Sandbox in your own infra: https://nodeops.network/createos/docs/Sandbox/Bring-Your-Own-Storage
Top comments (0)