sandbox — Engineering Performance
10 engineers all time · Jan 2026 – Aug 2026 · built 2026-09-08 · GitHub
Performance snapshot
Today's rolling 90-day reading for sandbox, compared with the start of the series. Pick a window to move that comparison point.
Avg. perf / dev / mo
+30.0%
0.37 → 0.49 ETV
Active engineers
−40.0%
10.0 → 6.0
Features
−5.0pp
50.3% → 45.3%
vs. Vercel
0.12x
0.12x → 0.12x · −88% below
sandbox vs. Vercel
Per-engineer ETV for sandbox against Vercel as a whole. Both lines are 90-day rolling averages scaled to a 30-day month, so they share one axis and can be read against each other at any point. Pick a window to zoom the chart to it.
Performance Composition
Each month's output split by type of work: Features (new value), Maintenance (sustaining systems), Tests, Docs, and Fixes (rework). The yellow line is output per engineer, so when it rises each engineer is delivering more, whatever the team size did. Unit: Engineering Throughput Value (ETV).
Engineering capacity
Effective engineers behind sandbox, against its pre-AI baseline. Each subject has its own: sandbox's is 0.37 ETV / dev / mo, its first reading in April 2026. Per-engineer ETV divided by that gives a capacity multiple, and that multiple applied to the engineers active in the trailing 90 days turns it into engineer-equivalents. The line is the real headcount, so the gap between line and area is what the leverage is worth. Because each baseline is its own, every subject opens at 1.0x on its first day: multiples measure improvement and are not comparable between subjects.
Knowledge concentration
How dependent is this repo on a small number of engineers? Higher top-1 share = higher key-person risk.
Marc Codina owns 54.2 % of commits.
Behind the numbers
Written summary of the work completed each month.
No monthly reports available yet.
Most impactful commits
Top 10 by ETV in the all-time window.
- 2.9ETVfeat(sandbox-mock): add @vercel/sandbox-mock package (#245) Testing code that uses `@vercel/sandbox` means provisioning real sandboxes — slow, network-bound, and requiring credentials. In order to do that, customers must mock the whole Sandbox object, and they cannot run the integration tests until they go live (or they use real sandboxes). This new package, `@vercel/sandbox-mock`, provides a mock that aims to run a local sandbox with some constraints. We are mocking the API layer, and instead of relying into the Vercel services, we mock it by using `just-bash`. We have also configured changeset to maintain version parity between `@vercel/sandbox` and `@vercel/sandbox-mock`, to ensure that users and agents install the mock with the correct signatures. Testing --- - Each file that I have added contains unit tests (unless it is just types or interfaces). - There is a `packages/vercel-sandbox-mock/tests/compat.test.ts` test file which attempts to run the mock and real sandboxes, to ensure there is compatibility. - I ran the integration tests locally to ensure the compatibility tests work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>Marc Codina · 96aa20fa · 2026-07-20
- 1.7ETVfeat(vercel-sandbox): support named sandboxes and sessions (#71) Important --- - This is PR contains the `changeset` required to ship a pre-release (`beta`) package with a breaking change. Merging the PR will not automatically create the pre-release tag. - This PR will be merged to `named-sandboxes`, not the main branch. - All the changes from `sandbox` package are to make it compile. We are not yet supporting these changes in the CLI; this PR is only about the `@vercel/sandbox` package. Breaking Changes --- **1:** `Sandbox.get()` - parameter renamed and semantics changed. - Before: `Sandbox.get({ sandboxId: string })` - After: `Sandbox.get({ name: string, resume?: boolean }`. The sandbox will also be automatically resumed if it was stopped. **2:** `Sandbox.list()` - the return items have changed. This method returns the list of sandboxes, and we are missing the following fields: - `id` (we use `name` instead) - `requestedAt`: the sandbox does not request a session. - `status`: it is specific from the session. (WIP - Working on providing this one) **3:** `Sandbox.list()` - the pagination has changed. We are using a cursor based token instead. - Before, the pagination returned: `{ count: number, next: number | null, prev: number | null }` - After, the pagination returns: `{ count: number, next: string | null, total: number }` **4:** Auto-resume any operation. - Before: If a sandbox session was stopped, all operations threw an error. - After: If the session is stopped/stopping, operations silently create a new session and retry. **5:** Timeout / NetworkPolicy are not from the session Before, the Timeout / NetworkPolicy parameters where from the session, and were automatically updated when the methods `extendTimeout()` or `updateNetworkPolicy()` were called. Now, the returned ones are from the NamedSandbox (base values to create a sandbox), and do not reflect the updated values for that session. **6:** Create - semantics changed - Before: `Sandbox.create()` - After: `Sandbox.create({ name: string, snapshotOnShutdown?: boolean }`. The sandbox will also be automatically persistent by default. Non-breaking changes --- ### New methods on Sandbox - currentSession() — returns the underlying Session instance. - update(params) — PATCH the named sandbox config (resources, runtime, timeout, networkPolicy). - delete(opts?) — delete the named sandbox (with optional preserveSnapshots). - listSessions(params?) — list all sessions created for this named sandbox. - listSnapshots(params?) — list all snapshots belonging to this named sandbox. ### New getters on Sandbox - name, snapshotOnShutdown, region, vcpus, memory, runtime - Aggregate usage metrics: totalEgressBytes, totalIngressBytes, totalActiveCpuDurationMs, totalDurationMs - updatedAt ### New query filters - listSandboxes() now accepts an optional name param to filter by named sandbox. - listSnapshots() now accepts an optional name param to filter by named sandbox. --------- Co-authored-by: Tom Lienard <tom.lienrd@gmail.com>Marc Codina · b4d5514d · 2026-03-03
- 1.2ETVfeat(sandbox,vercel-sandbox): multi-region support (#301) Enable multi-region for sandboxes and snapshots. When we show the region, if it is not defined, we default to `iad1`, which is where we place sandboxes/snapshots by default. Example of creating a sandbox (now we also show the region): ``` ❯ sandbox create ✅ Sandbox silver-colossal-wren-WU4M8Y created. │ team: marc-codina-enhanced-vtest314 │ region: iad1 │ project: my-sandbox-app-tmp ╰ connect with: sandbox ssh silver-colossal-wren-WU4M8Y ``` Example of creating a sandbox with failover regions: ``` sandbox create --region cle1 --failover-regions sfo1,iad1 ✅ Sandbox scarlet-vitreous-woodpecker-kkjYfK created. │ team: marc-codina-enhanced-vtest314 │ region: cle1 │ project: my-sandbox-app-tmp ╰ connect with: sandbox ssh scarlet-vitreous-woodpecker-kkjYfK ``` Example of updating an existing sandbox region and failover regions: ``` ❯ sandbox config list crimson-chronic-mackerel-pbh3WM FIELD VALUE Region cle1 Failover regions sfo1, iad1 ... ❯ sandbox config failover-regions crimson-chronic-mackerel-pbh3WM sfo1 ✅ Configuration updated for sandbox crimson-chronic-mackerel-pbh3WM ╰ failover-regions: sfo1 ❯ sandbox config region crimson-chronic-mackerel-pbh3WM iad1 ✅ Configuration updated for sandbox crimson-chronic-mackerel-pbh3WM ╰ region: iad1 ❯ sandbox config list crimson-chronic-mackerel-pbh3WM FIELD VALUE Region iad1 Failover regions sfo1 ... ``` Notes: - We are not validating the regions, so that if in the future we add more, old sandbox versions keep working. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Tom Lienard <tom.lienrd@gmail.com>Marc Codina · 2997ecc3 · 2026-08-20
- 0.9ETVfeat(sdk): update to V2 API schema for network policies (#174) Follow-up to https://github.com/vercel/sandbox/pull/173, update the network policies requests to use the V2 schema, which supports matchers and forward URLs For now, the API responses are still under the V1 schema, because changing it would be a breaking change. Only the requests are using the V2 schema with this PR, which greatly simplifies `toAPINetworkPolicy`Tom Lienard · f3b01827 · 2026-05-06
- 0.9ETV[persistent-sandboxes] Use the V2 endpoints instead of V1 (#87) Multiple changes to ensure there is compatibility: - Do not use any `v1` endpoint; use the `v2` endpoints instead. - Adapt to these new endpoints (modified requests and responses). - Rename `sandboxId` to `sessionId` - Rename `namedSandbox` to `sandbox` - Rename `sandbox` (old term) to `session`Marc Codina · 087d659d · 2026-03-13
- 0.9ETVAdd `node:fs` implementation on `Sandbox` (#112) This PR introduces a node:fs-compatible API for the Vercel Sandbox environment. The goal is to provide a familiar, minimal subset of Node’s filesystem interface that works within the constraints of the Sandbox runtime, enabling easier portability of existing Node.js code and libraries. In particular provide something we can pass directly around with `just-bash` as an `fs` implementation without needing their `Sandbox` primitive wrapper. --------- Co-authored-by: Nathan Rajlich <n@n8.io>LukePS · 42515e1e · 2026-04-13
- 0.8ETVSupport "use workflow" serialization for Sandbox and Command (#72)Malte Ubl · 772989c5 · 2026-03-27
- 0.8ETVfeat(sandbox): add multi-user and group management (#116) Adds `createUser`, `asUser`, `createGroup`, `addUserToGroup`, and `removeUserFromGroup` to the `Sandbox` class, plus a new `SandboxUser` class that wraps sandbox operations to run in a specific user's context. ## Creating users and running commands ```ts import { Sandbox } from '@vercel/sandbox'; const sandbox = await Sandbox.create(); // Create a user with isolated home directory at /home/alice const alice = await sandbox.createUser("alice"); alice.username; // "alice" alice.homeDir; // "/home/alice" // Commands run as alice, cwd defaults to /home/alice const whoami = await alice.runCommand("whoami"); await whoami.stdout(); // "alice\n" const pwd = await alice.runCommand("pwd"); await pwd.stdout(); // "/home/alice\n" // Pass environment variables const cmd = await alice.runCommand({ cmd: "node", args: ["-e", "console.log(process.env.SECRET)"], env: { SECRET: "hunter2" }, }); // Override working directory await alice.runCommand({ cmd: "ls", cwd: "/tmp" }); // Escalate to root when needed await alice.runCommand({ cmd: "dnf", args: ["install", "-y", "git"], sudo: true, }); // Detached mode for long-running processes const server = await alice.runCommand({ cmd: "node", args: ["server.js"], detached: true, }); // ... later await server.kill("SIGTERM"); ``` ## File operations scoped to the user ```ts const alice = await sandbox.createUser("alice"); // Relative paths resolve to /home/alice, files owned by alice:alice await alice.writeFiles([ { path: "app.js", content: Buffer.from('console.log("hi")') }, { path: "data/config.json", content: Buffer.from("{}") }, ]); // Read files back const buf = await alice.readFileToBuffer({ path: "app.js" }); buf?.toString(); // 'console.log("hi")' // Stream reads const stream = await alice.readFile({ path: "app.js" }); // Absolute paths work too await alice.writeFiles([ { path: "/opt/alice/data.bin", content: Buffer.from([0x00, 0xff]) }, ]); // Create directories owned by the user await alice.mkDir("projects/my-app"); ``` ## File isolation between users ```ts const alice = await sandbox.createUser("alice"); const bob = await sandbox.createUser("bob"); await alice.writeFiles([ { path: "secret.txt", content: Buffer.from("alice only") }, ]); // Bob cannot read alice's files const cat = await bob.runCommand({ cmd: "cat", args: ["/home/alice/secret.txt"], }); cat.exitCode; // non-zero, Permission denied // Bob cannot list alice's home directory const ls = await bob.runCommand({ cmd: "ls", args: ["/home/alice"] }); ls.exitCode; // non-zero, Permission denied // Bob cannot write to alice's home directory const touch = await bob.runCommand({ cmd: "touch", args: ["/home/alice/hacked.txt"], }); touch.exitCode; // non-zero // Each user reads their own files normally const content = await alice.readFileToBuffer({ path: "secret.txt" }); content?.toString(); // "alice only" ``` ## Group management with shared directories ```ts // Create a group with shared dir at /shared/devs (setgid 2770) const devs = await sandbox.createGroup("devs"); devs.sharedDir; // "/shared/devs" // Add users to the group await sandbox.addUserToGroup("alice", "devs"); await sandbox.addUserToGroup("bob", "devs"); // Or use convenience methods on SandboxUser await alice.addToGroup("devs"); // Files in the shared dir automatically inherit group ownership await alice.runCommand({ cmd: "bash", args: ["-c", 'echo "shared data" > /shared/devs/notes.txt'], }); const notes = await bob.runCommand({ cmd: "cat", args: ["/shared/devs/notes.txt"], }); await notes.stdout(); // "shared data\n" // Non-members are blocked const charlie = await sandbox.createUser("charlie"); const blocked = await charlie.runCommand({ cmd: "ls", args: ["/shared/devs"], }); blocked.exitCode; // non-zero, Permission denied // Remove from group revokes access await sandbox.removeUserFromGroup("alice", "devs"); ``` ## Using asUser for pre-existing users ```ts const existing = sandbox.asUser("bob"); await existing.runCommand("whoami"); ``` ## Multi-user AI agent example ```ts const sandbox = await Sandbox.create(); const researcher = await sandbox.createUser("researcher"); const coder = await sandbox.createUser("coder"); const reviewer = await sandbox.createUser("reviewer"); await sandbox.createGroup("project"); await sandbox.addUserToGroup("researcher", "project"); await sandbox.addUserToGroup("coder", "project"); await sandbox.addUserToGroup("reviewer", "project"); // Researcher writes findings to shared dir await researcher.runCommand({ cmd: "bash", args: ["-c", 'echo "API spec v2" > /shared/project/spec.txt'], }); // Coder reads spec and writes code in their own home const spec = await coder.runCommand({ cmd: "cat", args: ["/shared/project/spec.txt"], }); await coder.writeFiles([ { path: "app.js", content: Buffer.from(`// Based on: ${await spec.stdout()}`) }, ]); // Reviewer can read the shared spec but not coder's private files const blocked = await reviewer.runCommand({ cmd: "cat", args: ["/home/coder/app.js"], }); blocked.exitCode; // non-zero, isolation enforced ``` ## Implementation notes - Purely SDK-side, no backend API changes. Uses `runCommand` with `sudo` under the hood. - Command wrapping: `sudo -u <user> -- bash -c 'cd <dir> && exec "$@"' _ <cmd> <args>` - `writeFiles` stages to `/tmp` then `sudo mv` + `chown` (backend tar extraction can't write to user home dirs). - `readFile`/`readFileToBuffer` use `sudo cat` (backend read process can't traverse user home dirs). - `mkDir` uses `sudo mkdir` for the same reason. - Username/group validation prevents command injection. - Home dirs: `770`, `vercel-sandbox` added to each user's group. - Shared group dirs: setgid `2770`, files inherit group ownership. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Marc Codina <marc.codina@vercel.com>Malte Ubl · c8678da4 · 2026-07-13
- 0.7ETVfeat(sdk): add `defineSandboxProxy` helper (#188) This PR adds a new `defineSandboxProxy` helper in `@vercel/sandbox/proxy` to integrate [network policies forwarding rules](https://vercel.com/changelog/vercel-sandbox-firewall-now-supports-request-proxying-and-filtering) easily within a Vercel Function, or any other platform that supports the Web Handlers syntax (`Request`/`Response` objects) We use `jose` to verify the OIDC token, then extract the metadata (original host, scheme, port, path, and source team/project/sandbox ids) before calling a user-defined Web Handler. If the request cannot be authorized, we return a 403 by default, but the user can also override this behavior with a separate Web Handler. A new `Request` is constructed to correspond to the original sandbox request before being proxied by the sandbox firewall Example usage in a Vercel Function: ```ts export default { fetch: defineSandboxProxy(async (request, meta) => { // meta contains the original host & source team/project/sandbox ids console.log(meta) // return a custom response, or proxy upstream: return await fetch(request) }, (request, error) => { // optional, handle any authorization error return new Response("Forbidden", { status: 403 }) }) } ```Tom Lienard · 88cfb449 · 2026-05-20
- 0.6ETVfeat(vercel-sandbox,sandbox): support pagination when listing sandboxes, sessions, snapshots (#163) Introduce pagination support for the CLI/SDK, so that customers and agents can iterate over existing sandboxes, snapshots and sessions. CLI --- The CLI now supports two new parameters to control the pagination: - `--limit` - `--cursor` Also, in the list commands that support pagination, we are also returning the `More results: ...` at the end, so that humans and agents know which commands they should run next if they want to iterate over the results. ``` > sandboxes list --limit 2 ... More results: sandbox list --limit 2 --cursor eyJvZmZzZXQiOjJ9 > sandbox sessions list my-sandbox --limit 2 ... More results: sandbox sessions list my-sandbox --limit 2 --cursor eyJvZmZzZXQiOjJ9 > sandbox snapshots list --name my-sandbox --limit 2 ... More results: sandbox snapshots list --name my-sandbox --limit 2 --cursor eyJvZmZzZXQiOjJ9 ``` SDK --- SDK introduces a small breaking change for the beta customers. When executing any of these methods: - `await Sandbox.list()` - `await sandbox.listSessions()` - `await sandbox.listSnapshots()` - `await Snapshot.list()` We are going to return an Iterable. To match the previous behavior, you will need to convert these into an array. For example: ``` const sandboxes = await (await Sandbox.list()).toArray(); ``` If you want to iterate through the results and automatically let the SDK query the pages when required: ``` const result = await Sandbox.list({ namePrefix: "ci-", limit: 20 }); # Iterate item by item. for await (const sandbox of result) { console.log(sandbox.name, sandbox.status); } # Iterate page by page. for await (const page of result.pages()) { console.log(`Got ${page.sandboxes.length} sandboxes, next=${page.pagination.next}`); } ``` To support pagination, these previous 4 methods will support two new optional parameters: - `limit`: page size. Maximum is 100. - `cursor`: starting point. The first page is fetched with this cursor; from there the paginator follows pagination.next automatically.Marc Codina · d9f6e1ca · 2026-04-28