stackbox
One OpenSandbox-compatible endpoint for E2B, Modal, Daytona, Vercel, Cloudflare and your own OpenSandbox servers. Each sandbox goes to the cheapest or fastest provider that can run it, quotas are pooled across accounts, and failures fail over without duplicates or leaked sandboxes.
Clients don't change. The stock OpenSandbox SDKs (Python, Go, Kotlin, TS, C#), the osb CLI, and
Harbor's opensandbox environment all talk to it as if it were one OpenSandbox server.
Quickstart
uv tool install "stackbox[all]" # or: pip install "stackbox[all]"
stackbox login # choose a provider, paste its key; checked, then saved
stackbox doctor # checks each provider end to end, says what to fix
stackbox prepare ubuntu:24.04 # makes the image fast to start on every provider
stackbox serve # http://127.0.0.1:8090
No config file is needed:
- providers are detected from
stackbox loginor their usual environment variables; - prices and plan limits come from a built-in catalog;
- the
execdbinary each sandbox runs is downloaded automatically.
stackbox init writes a stackbox.toml when you want to customise.
Python with the stock OpenSandbox SDK. The same code is a script you can run: uv run examples/quickstart.py (uv installs the SDK for it), or pip install opensandbox first. The snippet uses top-level await, as in Jupyter or python -m asyncio:
from opensandbox import Sandbox
from opensandbox.config import ConnectionConfig
router = ConnectionConfig(domain="http://127.0.0.1:8090", protocol="https")
sandbox = await Sandbox.create("ubuntu:24.04", connection_config=router)
print((await sandbox.commands.run("echo hello")).logs.stdout[0].text)
await sandbox.kill()
Harbor (examples/harbor.yaml): set the environment to opensandbox, domain to the
router, and protocol: https. Then:
harbor run -c examples/harbor.yaml
Routing
Pick a default strategy with [routing] strategy or serve --strategy, and override it per
request, OpenRouter-style:
| Strategy | Picks |
|---|---|
balanced (default) |
the best mix of price and startup time |
cost |
the lowest list price for what the provider would bill |
latency |
the lowest learned time-to-sandbox for this image on this provider |
priority |
backends in configured priority order |
Per-request preferences go in headers (SDK: ConnectionConfig(headers=…)) or in sandbox
metadata (Harbor: kwargs.metadata):
| Header | Metadata key | Meaning |
|---|---|---|
X-Stackbox-Strategy: cost |
stackbox.stacktrace.dev/strategy |
override the strategy |
X-Stackbox-Providers: e2b,modal |
stackbox.stacktrace.dev/providers (e2b.modal) |
only these providers; without a strategy, try them in this order |
X-Stackbox-Max-Price: 0.10 |
stackbox.stacktrace.dev/max-price |
skip providers above this USD per sandbox-hour |
X-Stackbox-Backend: e2b |
stackbox.stacktrace.dev/backend |
pin to one provider, no fallback |
Why a sandbox goes where it does: stackbox route [IMAGE] (or GET /v1/route?image=…) lists
every provider in order with its price, expected startup time and status, for example:
- next placement goes here;
- full: 5/5 (learned from provider refusals);
- not prepared; run
stackbox prepare …; - too expensive.
Errors carry the same per-provider reasons and a hint.
How the routing behaves:
- Learns startup time: the latency estimate is an average of observed creates per provider and image, weighted toward recent ones. It survives restarts, and each provider gets 3 measured starts before it can lose on its default estimate.
- Discovers quotas: a provider's quota refusal (Daytona's memory cap, E2B's rate limit, Modal's
resource limits, Cloudflare having no instances to hand out) is not a failure.
- The router caps that provider at its current count, growing the cap back by one every 15 s.
- New creates spill to other providers or wait.
- Refusals don't count toward the circuit breaker.
- Fails over only when it's safe:
- A clean failure (nothing was created) moves on to the next provider.
- An ambiguous one (timeout, 5xx) first checks the provider for the sandbox and adopts or deletes what it finds.
- If the outcome can't be determined, it returns
503 STACKBOX::OUTCOME_UNKNOWNrather than risk a duplicate.
- Breaker and retries: a per-backend circuit breaker demotes a backend after repeated failures, and retries go to other backends first.
CLI
| Command | What it does |
|---|---|
stackbox login [PROVIDER] |
save a provider's credentials after checking them with the provider; Vercel lets you choose the team and project, Cloudflare deploys the bridge Worker (--with-token reads stdin, --deploy for scripts) |
stackbox logout PROVIDER |
remove saved credentials |
stackbox providers |
supported providers, list prices, plan limits, where each one's credentials come from, and live status |
stackbox init |
write stackbox.toml for the providers you're logged in to; --add adds new logins to an existing file |
stackbox doctor |
per provider: SDK, credentials, API reachable, execd, image prepared; each failure comes with its fix |
stackbox prepare IMAGE… |
build templates and image layers, push to Vercel's registry, add the image to the Cloudflare bridge; -p limits to some providers |
stackbox serve |
run the server (--host, --port, --strategy) |
stackbox route [IMAGE] |
where a sandbox would go right now, and why (--strategy, --providers, --max-price, --cpu, --memory) |
stackbox images [IMAGE] |
prepared images per provider |
stackbox sandboxes |
sandboxes the running server holds, with provider and cost (--all includes ended ones) |
stackbox placement ID |
where one sandbox went, every attempt, and its cost |
stackbox usage |
sandbox-hours, vCPU-hours, GiB-hours and estimated cost per provider (--since 24h) |
stackbox providers also shows live status (held/limit, placements) when a server is running.
Every command takes -c FILE, and the read commands take --json. Config is found in this order:
-c;$STACKBOX_CONFIG;./stackbox.toml;- auto-detection from credentials.
A config file lists the providers to use, so a provider you log in to later isn't used until it's
in the file. stackbox login offers to add it, and stackbox init --add adds every one that's
missing.
Credentials. stackbox login saves them in ~/.config/stackbox/credentials.toml
($XDG_CONFIG_HOME, or $STACKBOX_CREDENTIALS), readable only by you. Each provider's own
environment variables (E2B_API_KEY, VERCEL_TOKEN, …) still work and take precedence, which is
what CI should use:
echo "$E2B_API_KEY" | stackbox login e2b --with-token
printf 'VERCEL_TOKEN=%s\nVERCEL_TEAM_ID=%s\nVERCEL_PROJECT_ID=%s\n' "$TOKEN" "$TEAM" "$PROJECT" | stackbox login vercel --with-token
doctor exits 0 when everything passes, 1 when some providers have problems, and 2 when nothing
is usable.
Providers
| Kind | Credentials | Images | Notes |
|---|---|---|---|
e2b |
E2B_API_KEY |
any image; prepared as an E2B template with execd (about 45 s, once) |
Firecracker; a template fixes vCPU and memory |
modal |
modal token set, or MODAL_TOKEN_ID + MODAL_TOKEN_SECRET |
any image; the execd layer is built once |
gVisor; 1 vCPU = half a Modal core |
daytona |
DAYTONA_API_KEY |
any image, as a declarative image Daytona builds once and caches | the account memory quota is discovered automatically |
vercel |
VERCEL_TOKEN + VERCEL_TEAM_ID + VERCEL_PROJECT_ID |
Vercel's registry only; prepare pushes image + execd there (needs Docker once) |
bills active CPU only; Hobby caps sandboxes at 45 min |
cloudflare |
CLOUDFLARE_BRIDGE_URL + CLOUDFLARE_BRIDGE_TOKEN |
prepare adds them by redeploying the bridge (needs Node.js and Docker) |
stackbox login cloudflare deploys the bridge Worker (needs Node.js and Docker); bills active CPU only |
opensandbox |
url (+ api_key) in config |
the server pulls them | your own OpenSandbox servers on Docker or Kubernetes |
On hosted providers the router emulates the OpenSandbox lifecycle on the provider's SDK and runs
OpenSandbox's execd in every sandbox, with a per-sandbox token. GET /sandboxes/{id}/endpoints/44772
returns the provider's URL and the headers to send. Command and file traffic goes from the client
to the sandbox, not through the router. Cloudflare is the exception: it goes through the bridge
Worker.
Configuration
Every field is optional except kind; the provider catalog fills in the rest. See
examples/all-providers.toml for every provider, and stackbox init for a starter.
[server]
api_key = "env:STACKBOX_API_KEY" # required to listen beyond 127.0.0.1
[routing]
strategy = "balanced"
[[backends]]
kind = "e2b"
max_active = 50 # your plan's concurrency
[[backends]]
kind = "modal"
priority = 10
- Secrets:
"env:NAME"values (or"env:A|B") are read from the environment, so secrets stay out of config files. - Common backend fields:
max_active(concurrency);priority;cost_per_vcpu_hourandcost_per_gib_hour;cpu_billed_fraction, for providers that bill only active CPU;max_timeout_s;default_cpuanddefault_memory_gib;auto_prepare;execd_path, if you'd rather supplyexecdyourself.
HTTP API
Hosted at https://stackbox.stacktrace.dev, or wherever you run stackbox serve. Every route
except GET /health needs the API key in OPEN-SANDBOX-API-KEY.
OpenSandbox API (what the SDKs, Harbor and osb use, unchanged):
| Route | |
|---|---|
POST/GET /v1/sandboxes, GET/DELETE /v1/sandboxes/{id} |
create, list, get, delete |
/v1/sandboxes/{id}/endpoints/{port}, …/renew-expiration, … |
forwarded to the provider; hosted providers implement endpoints and renew-expiration and return 501 for the rest |
/v1/snapshots, /v1/templates |
501 STACKBOX::UNSUPPORTED; prepare images with POST /v1/images instead |
stackbox API:
| Route | |
|---|---|
GET /v1/providers, GET /v1/providers/{name} |
pricing, capacity (held, limit, learned quota), health, learned startup per image, prepared images, usage |
GET /v1/route?image=…&strategy=…&providers=…&max_price=…&cpu=…&memory_gib=… |
where a sandbox would go right now, and why, for every provider (dry run) |
GET /v1/placements/{sandbox_id} |
where a sandbox went, every attempt (outcome, error, duration), and its cost |
GET /v1/images?image=…, POST /v1/images {image, providers?} |
prepared images per provider; prepare an image |
GET /v1/usage?since=24h&until=…&group_by=provider|sandbox |
sandbox-hours, vCPU-hours, GiB-hours, estimated cost |
POST /v1/admin/reconcile |
run cleanup now |
Errors are JSON: {code: "STACKBOX::…", message, details, hint}. The message includes the hint,
since SDKs show only the message. SANDBOX::NOT_FOUND keeps OpenSandbox's standard code.
Reliability
- Ledger: a SQLite ledger records intent before every upstream create, and every attempt's outcome.
- Reconciler: runs periodically and on
POST /v1/admin/reconcile:- deletes orphans from ambiguous creates;
- expires sandboxes that vanished upstream;
- deletes leaked sandboxes carrying this router's managed tag.
- Ownership: the tag holds a per-ledger instance id, so routers sharing an account never touch each other's sandboxes.
Verified
All of these ran on real accounts with Harbor trials (an oracle agent, ubuntu:24.04, 1 vCPU,
2 GiB). Full write-ups: bench/LIVE.md and bench/LIVE-MORE.md; the local fault-injection
benchmark is in bench/RESULTS.md.
- Reliability: every router arm passed 48/48 with no leaked sandboxes. That covers E2B, Modal, Daytona, Vercel, Cloudflare, cost and latency routing, and quota pooling.
- Strategy decides placement:
costchose E2B, 28% cheaper than Modal.latencychose the fastest-starting provider and followed it as providers sped up and slowed down. - Quotas:
- Pooling two 4-sandbox quotas: 36 s, versus 69 s on one provider.
- Daytona's 10 GiB account quota: 67–71 s through the router, versus 199 s with Harbor's own Daytona environment; 40–56 s with E2B taking the overflow.
- Against Harbor's own provider environments:
- E2B: 14% faster and 16% cheaper, traced to Harbor's E2B file transfers.
- Vercel: 48/48 through the router, versus 10/48 native (a per-trial registry-token rate limit).
- Cloudflare: Harbor has no environment of its own; the router is how it gets there.
Router create p50: Modal 0.6–0.8 s, Vercel 0.9 s, E2B 1.1–1.4 s, Cloudflare 1.5 s, Daytona 2.8–3.5 s.
Two public benchmarks, with their setup and caveats:
- Terminal-Bench 2.0 (
bench/TB2.md): 12 tasks with Harbor's oracle agent, 12/12 on every arm. stackboxcostmatched Harbor's best environment (E2B: 104 s vs 105 s).balancedtook 139 s where Harbor's Daytona environment took 377 s, queued behind the account quota. - ComputeSDK Burst TTI (
bench/computesdk/README.md): within an account's limits a provider is faster used directly (stackbox adds 0.3 s on Modal, 2 s on E2B, 5 s on Daytona to time to first command). At 100 sandboxes at once stackbox completed all of them, while E2B, Daytona and Vercel used directly on our accounts completed 39%, 30% and 16%.
Limits and next steps
- Startup costs more through stackbox than direct, mostly on E2B and Daytona: starting
execdand the first commands in a freshly started custom image. Next: snapshot a runningexecdinto the template where the provider allows it (bench/computesdk/README.md). latencyexplores each provider for each image, so a run with many distinct images spends most placements exploring. Next: share startup estimates across images on the same provider.- A single command streaming for about six minutes lost its connection on E2B (not yet
explained; see
bench/TB2.md). - Latency routing learns startup time, not task runtime. It picked Vercel for its fast starts, but Vercel trials ran longer. Next: learn per-provider runtime from completed sandboxes.
- Hosted providers lack some OpenSandbox features: no egress sidecar (network policy),
pause/resume or snapshots. Only E2B exposes ports other than
execd's, and Modal can't extend a running sandbox. - Cost is an estimate from list prices and allocated resources, not invoices. Active-CPU
billing is approximated with
cpu_billed_fraction. - Cloudflare adds a hop:
execdtraffic goes through the bridge Worker, at about 165 ms per command versus about 110 ms elsewhere. - Listing doesn't scale yet: the list endpoint queries every backend on each call.
- Later: a Kubernetes backend, region and GPU matching, and per-tenant quotas.
Development
git clone https://github.com/stacktrace-ai/stackbox && cd stackbox
uv sync --all-extras
uv run pytest -q # all tests run against in-memory fakes
uv run ruff check src tests bench scripts examples && uv run ruff format
Contributor guidelines and the developer-experience principles are in CLAUDE.md. Releases ship by
pushing a v<version> tag; see docs/releases/ and the release-stackbox skill.
License
Metadata
Release files for stackbox 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| stackbox-0.3.0.tar.gz | 111.6 kB | With provenance | Details for stackbox-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| stackbox-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for stackbox-0.3.0-py3-none-any.whl |
Total release size: 233.1 kB
Release files / stackbox-0.3.0.tar.gz
| Download URL | stackbox-0.3.0.tar.gz |
|---|---|
| Size | 111.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
855ed51281dd844c80fac464971a8aca6bf9d8355f25cefa207ecf4a1bc73c54
|
|
BLAKE2b-256 checksum How to use checksums |
56a5f16d9e95417c5bf1ff54b748db3bc7a239d7b60b1b10690bef1d34e50e6e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logRelease files / stackbox-0.3.0-py3-none-any.whl
| Download URL | stackbox-0.3.0-py3-none-any.whl |
|---|---|
| Size | 121.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4f242c5b695b3d9ab73b309a87a9d731f0098e8c36a793ac597932483d396246
|
|
BLAKE2b-256 checksum How to use checksums |
eb815481f95118e3fb1bc07c530e80d4967bf524a65a0eb3dd32e92f64ad2800
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logSecurity for stackbox 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for stackbox-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.