The “drop-in replacement” framing is approximately true for the
common E2B SDK workflow (create → run code → close) and approximately
false for anything observability-adjacent. The CubeAPI/README.md
support matrix is the canonical source.
Implemented endpoints
From CubeAPI/README.md + cross-checked against CubeAPI/src/routes.rs:
| Method | Path | Role in common E2B flow |
|---|---|---|
| GET | /health | readiness probe |
| POST | /sandboxes | Sandbox.create(template=…) |
| GET | /sandboxes | Sandbox.list() |
| GET | /v2/sandboxes | v2 list w/ filters |
| GET | /sandboxes/:id | Sandbox.get(id) |
| DELETE | /sandboxes/:id | sandbox.close() |
| POST | /sandboxes/:id/pause | snapshot/pause for later resume |
| POST | /sandboxes/:id/resume | (deprecated; prefer connect) |
| POST | /sandboxes/:id/connect | attach client, auto-resume from pause |
This is the lifecycle. Sandbox.create → run_code → close only touches
POST /sandboxes, the WebSocket connect, and DELETE /sandboxes/:id.
All three are in the ✅ column.
Not implemented (501 / not registered)
| Method | Path | When you’d miss it |
|---|---|---|
| GET | /sandboxes/:id/logs (v1) | Streaming sandbox-side log collection |
| GET | /v2/sandboxes/:id/logs | Same, cursor-paginated |
| POST | /sandboxes/:id/timeout | Absolute TTL management |
| POST | /sandboxes/:id/refreshes | Relative TTL extend |
| POST | /sandboxes/:id/snapshots | Durable snapshot creation (distinct from pause) |
| GET | /sandboxes/snapshots | List team-wide snapshots |
| GET | /sandboxes/:id/metrics | CPU/memory gauges per sandbox |
| PUT | /sandboxes/:id/network | Live egress policy update |
If you’re using e2b-code-interpreter for straightforward “run this
Python and give me the result” workflows, none of these show up. If
you’re integrating E2B into an operator dashboard, the metrics + logs gap
is load-bearing.
WebSocket surface
E2B uses WebSocket for the Sandbox.run_code streaming path. axum 0.7
with the ws feature handles this; CubeAPI implements it. The byte-level
protocol (JSON framing, message types) mirrors E2B’s. Client code written
against the E2B SDK does not notice the switch.
Authentication
From the quickstart:
export E2B_API_URL="http://127.0.0.1:3000"
export E2B_API_KEY="dummy"
In the open-source single-node install, E2B_API_KEY is effectively off.
Production deployments front CubeAPI with their own auth — CubeProxy is a
natural place.
What ports to FreeBSD
Axum and tokio are portable. CubeAPI does not touch Linux-specific
primitives in its HTTP layer; the calls it makes downward go to
CubeMaster over gRPC. So rather than cross-compile Tencent’s CubeAPI,
we built our own Axum service — e2b-compat/ — that speaks the same
E2B REST surface but dispatches downward into jails + bhyve rather
than containerd + cube-hypervisor. On honor, the E2B Python SDK
passes 10/10 of its API-server calls against this service, and the
envd-compat endpoint on :49999 streams run_code NDJSON via
jexec python3 — see
/appendix/run-code-protocol for the
wire format and the round-trip transcript.
The complexity in a FreeBSD port lives below the API layer, not at the API layer itself — as expected.
Caveats
”E2B” is a moving target
E2B’s own API has been shifting — endpoints added, versioned, deprecated. CubeAPI pins against a specific E2B version (implicitly, via the routes it chose to implement). If the E2B SDK introduces a new required endpoint, CubeAPI will either return 501 on that endpoint or lag behind until Cube adds it. “Drop-in” is pinned to a moment in time.
Template semantics
CUBE_TEMPLATE_ID in the quickstart is a Cube-specific ID that maps to
a stored VM template. E2B’s template field in Sandbox.create is
per-team on E2B cloud and maps to E2B-managed templates. Cube accepts
E2B-shaped template IDs by treating them as opaque strings and looking
them up in CubeMaster’s template registry. If you have existing
“template” IDs on E2B cloud and expect them to work unchanged
on Cube, they won’t — you upload templates to Cube separately.
Snapshots are not snapshots
E2B has two distinct concepts: /pause (stateful pause, live memory
snapshot) and /snapshots (create a durable snapshot image, e.g., to
fork from later). Cube implements the former and not the latter. If your
code calls sandbox.snapshot() expecting to fork later, it’ll get a 501.
If it calls sandbox.pause()/sandbox.resume(), it works.