# How Fast Are Burst Sandbox Creates? 20 and 40 at Once, Measured

> Measured on sandboxer.com production, Oct 31 2026, client in Ashburn VA, create→ready: 20 default sandboxes started at once in us-east were all ready with a p50 of 0.76 s and a max of 0.82 s; 40 at once with no region had a p50 of 1.87 s and a max of 2.00 s.

Source: https://sandboxer.com/guides/how-fast-are-burst-sandbox-creates. Updated 2026-10-31.

**Short answer:** Measured on sandboxer.com production, Oct 31 2026, client in Ashburn VA, create→ready: 20 default sandboxes started at once in us-east were all ready with a p50 of 0.76 s and a max of 0.82 s; 40 at once with no region had a p50 of 1.87 s and a max of 2.00 s.

Results vary with load, region and where your client is. This page has one run of each burst size, with the method and the script so you can rerun it.

## Results

Label: **measured on sandboxer.com production, Oct 31 2026, client in Ashburn VA, create→ready.** Size `default` (4 vCPU, 8 GB). Times are per sandbox, from the create call to ready.

| Burst | Region | Ran in | p50 | p90 | max | All ready in |
| - | - | - | - | - | - | - |
| 20 at once | `us-east` (requested) | 20 in us-east | 0.76 s | 0.81 s | 0.82 s | 0.83 s |
| 40 at once | none given (the API picks) | 30 in us-east, 10 in eu-central | 1.87 s | 1.96 s | 2.00 s | 2.01 s |

The 40-at-once run, split by region:

| Region | Sandboxes | p50 | p90 | max |
| - | - | - | - | - |
| `us-east` (nearest) | 30 | 1.90 s | 1.97 s | 2.00 s |
| `eu-central` (`regionSource: "burst"`) | 10 | 0.63 s | 0.87 s | 0.87 s |

All 60 sandboxes became ready; none failed.

## How to read these numbers

- **Both bursts were served from warm pools,** sandboxes booted ahead of time. A burst bigger than the free warm pools includes cold boots and takes longer, so don't extrapolate to larger bursts.
- **One run each, at a quiet hour.** These aren't averages over many runs, and other traffic can change them.
- **The client sat in the same data center as `us-east`,** so the network distance to `us-east` was about zero. From elsewhere, add your round-trip time.
- **Ready means the sandbox can run a command.** It doesn't include running one, a setup script or cloning repos.
- **Why 10 landed in `eu-central`:** the 40-at-once run gave no region, and once `us-east`'s free sandboxes were taken the rest went to the other region. Each of those reports `regionSource: "burst"`. Pass `region` to keep a burst in one region; see [regions](/docs/regions).

## Method

- **Client:** one Linux host in Ashburn, Virginia, in the same data center as the `us-east` region.
- **Firing:** raw HTTPS calls to the public REST API, no SDK or CLI. A Python 3 script (standard library only) starts N threads and releases them together with a barrier. Each thread sends `POST /api/v1/sandboxes` with `"type": "default"` (plus `"region": "us-east"` for the 20 run, no region for the 40), then `GET /api/v1/sandboxes/{id}?waitFor=ready`.
- **Ready:** the moment that GET answers. The server holds it open until the sandbox can run a command, which is how the SDKs, the CLI and MCP wait too. The time is the wall clock from just before the POST to that answer.
- **Cleanup:** each sandbox was deleted as soon as it was timed.
- **When:** Saturday, October 31, 2026: the 40 run at 11:15 UTC and the 20 run at 11:16 UTC, on shared production with no special setup.

## Rerun it

You need an API key that can create, read and delete sandboxes (see [API keys](/docs/api-keys)), and a plan whose limit on sandboxes at once is at least N (see [limits](/docs/limits)).

```bash
export SANDBOXER_API_KEY=sbx_…
python3 bench-burst.py -n 20 --type default --region us-east
python3 bench-burst.py -n 40 --type default
```

It prints p50, p90 and max overall and per region, and deletes every sandbox as it goes. The script:

```python title="bench-burst.py"
#!/usr/bin/env python3
"""bench-burst.py: N sandbox creates fired at once, each timed from its create call to ready.

    SANDBOXER_API_KEY=… python3 bench-burst.py [-n 20] [--type default] [--region us-east] [--api https://sandboxer.com/api/v1]

Each worker thread sends POST /sandboxes, then GET /sandboxes/{id}?waitFor=ready (the server answers the moment
the sandbox can run a command; the SDKs, CLI and MCP do the same), and records the wall time from just before the
create to the ready answer. All N creates are released together by a barrier. Each sandbox is deleted the moment it
is timed (or after --give-up seconds without becoming ready), so the test holds capacity for seconds, not minutes.
Prints p50/p90/max overall and per region the sandboxes landed in.
"""
import argparse
import json
import os
import statistics
import threading
import time
import urllib.request

ap = argparse.ArgumentParser()
ap.add_argument("-n", type=int, default=20)
ap.add_argument("--type", default="default")
ap.add_argument("--region", default="")
ap.add_argument("--api", default="https://sandboxer.com/api/v1")
ap.add_argument("--give-up", type=float, default=90, help="seconds before a create that isn't ready is deleted")
args = ap.parse_args()
key = os.environ["SANDBOXER_API_KEY"]


def call(method, path, body=None, headers=None, timeout=120):
    req = urllib.request.Request(args.api + path, method=method, data=json.dumps(body).encode() if body is not None else None)
    req.add_header("Authorization", "Bearer " + key)
    req.add_header("Content-Type", "application/json")
    req.add_header("User-Agent", "sandboxer-bench-burst/1")
    for k, v in (headers or {}).items():
        req.add_header(k, v)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return r.status, json.load(r)
    except urllib.error.HTTPError as e:
        try:
            return e.code, json.load(e)
        except Exception:
            return e.code, {}


results = [None] * args.n
barrier = threading.Barrier(args.n)


def worker(i):
    body = {"type": args.type, "ttlSeconds": 900, "name": f"bench-{int(time.time())}-{i}"}
    if args.region:
        body["region"] = args.region
    barrier.wait()
    t0 = time.monotonic()
    code, r = call("POST", "/sandboxes", body)
    sb = r.get("sandbox") or {}
    if code >= 300 or not sb.get("id"):
        results[i] = {"ok": False, "error": (r.get("error") or {}).get("code", code)}
        return
    created = time.monotonic() - t0
    sid = sb["id"]
    state = sb.get("state")
    while state not in ("running", "ready", "error", "cancelled", "failed") and time.monotonic() - t0 < args.give_up:
        code, r = call("GET", f"/sandboxes/{sid}?waitFor=ready", timeout=max(5, args.give_up - (time.monotonic() - t0)))
        sb = r.get("sandbox") or sb
        state = sb.get("state")
        if code >= 300:
            break
    elapsed = time.monotonic() - t0
    call("DELETE", f"/sandboxes/{sid}", headers={"X-Sandboxer-Confirm-Delete": sid})  # free its room at once
    results[i] = {"ok": state in ("running", "ready"), "id": sid, "s": elapsed, "accepted": created,
                  "region": sb.get("region"), "source": sb.get("regionSource"), "state": state}


threads = [threading.Thread(target=worker, args=(i,)) for i in range(args.n)]
wall0 = time.monotonic()
for t in threads:
    t.start()
for t in threads:
    t.join()
wall = time.monotonic() - wall0


def pct(xs, p):
    xs = sorted(xs)
    return xs[min(len(xs) - 1, max(0, round(p / 100 * len(xs) + 0.5) - 1))]


def line(label, rs):
    ts = [r["s"] for r in rs]
    return f"{label:<22} n={len(ts):<4} p50 {statistics.median(ts):6.2f} s  p90 {pct(ts, 90):6.2f} s  max {max(ts):6.2f} s"


ok = [r for r in results if r and r["ok"]]
bad = [r for r in results if not r or not r["ok"]]
print(f"{args.n} x {args.type} creates at once{' in ' + args.region if args.region else ' (no region given)'}: {len(ok)} ready, {len(bad)} failed; all ready in {wall:.2f} s")
if ok:
    print(line("all", ok))
    for reg in sorted({r["region"] for r in ok}):
        rs = [r for r in ok if r["region"] == reg]
        srcs = ",".join(sorted({str(r["source"]) for r in rs}))
        print(line(f"{reg} ({srcs})", rs))
for r in bad[:10]:
    print("failed:", r)
print("deleted", sum(1 for r in results if r and r.get("id")), "(each as soon as it was timed)")
```

## What this means for your code

- **Fire creates in parallel.** Each create is answered independently, so a loop that waits for one sandbox before creating the next is the slow part. The [sub-second creates guide](/guides/get-sub-second-sandbox-creates-from-the-sdk) shows parallel creates with the SDKs.
- **Pin a region when it matters.** Without `region`, a burst can spill to the other region. That's faster than waiting, but your sandboxes then run farther from you or your data.
- **Measure from where your code runs.** Add your own round trip to these numbers, or run the script from your host.

## FAQ

### How long does it take to start 20 sandboxes at once?

In our run (measured on sandboxer.com production, Oct 31 2026, client in Ashburn VA, create→ready), 20 `default` sandboxes in `us-east` were all ready within 0.83 s, with a p50 of 0.76 s.

### Why did some sandboxes in the 40 run start in eu-central?

No region was given, so after the free `us-east` sandboxes were taken the API placed the rest in `eu-central` and marked them `regionSource: "burst"`. Pass `region` to keep every sandbox in one region.

### Does ready include running my setup?

No. Ready means the sandbox can run a command. Setup scripts, repo clones and your first command come after.

### Can I run this benchmark myself?

Yes. Copy the script above, set `SANDBOXER_API_KEY` and run it with `-n` no larger than your plan's limit on sandboxes at once. It deletes each sandbox as soon as it's timed.

## Related

- [How fast does a cloud sandbox start, resume and fork?](/guides/how-fast-does-a-cloud-sandbox-start-resume-and-fork)
- [Get sub-second sandbox creates from the SDK](/guides/get-sub-second-sandbox-creates-from-the-sdk)
- [Choose a sandbox region](/guides/choose-a-sandbox-region-us-east-or-eu-central)
- [Regions](/docs/regions)
