abhishek.it
Back to writing
·
#cubesandbox#microvm#kvm#sandbox#ai-agents#benchmark#bare-metal#firecracker#e2b#hetzner#infrastructure

How fast can one box spin up a thousand sandboxes?

I put Tencent's CubeSandbox on a low-end dedicated box and measured every part of the spin-up path. Warm resume ~20 ms, cold create ~72 ms, a 40-wide cold burst at p50 ~490 ms (p99 over a second), a thousand create-delete cycles in 23 seconds, and a density wall at 165 concurrent. Plus how it lines up against the managed sandboxes for coding and RL, and why I ran it on bare metal.

July 2026. Real runs on one Hetzner AX41. Ryzen 5 3600, 6 cores / 12 threads, 62 GB, Ubuntu 26.04 with KVM. CubeSandbox v0.5.1, one-click install. Raw JSON kept, nothing mocked.

CubeSandbox is Tencent's microVM sandbox runtime for agents. KVM, not containers. It claims sub-60ms cold starts with hardware isolation. I wanted that number on hardware anyone can rent, so I grabbed a low-end dedicated box and measured the whole spin-up path myself. Cold create, warm resume, both under load, a thousand create-delete cycles, and the point where concurrent density stops.

A sandbox here is a small virtual machine with its own kernel, thrown away when you're done. That VM boundary is the point: code the model wrote can't reach the host. Two ways to get one running.

What a sandbox is, and the two ways it starts
one microVM per sandbox · cold create boots it, warm resume wakes a paused one
Cold createTemplatefrozen rootfsboot kernelmicroVM bootsguest + agentRunning sandbox~72 msWarm resumePaused snapshotVM memory savedrestore memoryUnpausemap pages backRunning sandbox~20 msEach sandbox is a real microVM with its own kernel: a hardware boundary around code the model wrote.
Warm resume
20 ms
paused snapshot, cache-hot
Cold · 1 create
72 ms
boot from template
1,000 cycles
23 s
create-delete, 40 workers
Concurrent alive
165
scheduler cap on this box

Two spin-up numbers, not one

There isn't one number. There are two, and they're 5x apart.

Cold create means schedule the sandbox, restore its rootfs, wait for the agent inside to answer. That's ~72 ms for one sandbox on this CPU. Warm resume is a different thing. Pause a sandbox, then resume it. CubeSandbox writes the paused VM's memory to disk, so resume is a disk-backed restore, and it came back in ~20 ms here with the snapshot hot in the host page cache. That's the path that beats 50 ms. Keep a small pool paused, resume on demand, and a sandbox feels instant. When the snapshots aren't cached the number climbs, which shows up at scale later.

Ideal creation time vs. what a batch does to it
single-sandbox and per-op latency by path · dashed line is the 50 ms mark
50 ms targetWarm resumecache-hot single20 msCold create · 1 userboot from template72 msCold · 12 at once= CPU thread count137 msCold · 40 at oncestampede on 6 cores490 ms

The word that matters is ideal. That 490 ms bar isn't a single sandbox's boot time. It's roughly the p50 wait when you fire 40 cold creates at once onto 12 hardware threads, and the p99 in that run runs past a second. I didn't profile enough to split queue time from slower service under contention. The point holds the other way round: a user who shows up alone sees 15 to 72 ms, and you only reach the high numbers by stampeding the box.

Why I used bare metal

CubeSandbox needs /dev/kvm. These are real microVMs with their own kernels, not containers, so you need hardware virtualization exposed to the OS. Native KVM on this box (svm plus a KVM kernel) gave me a controlled host with nothing between me and the metal. It's not a hard requirement, though: CubeSandbox ships a PVM mode that runs on ordinary x86-64 cloud VMs without exposed VT-x/AMD-V. Bare metal was just the clean way to measure.

Whatever the exact cause, throughput plateaued around 12 concurrent creates on this 6-core / 12-thread host. I didn't profile enough to pin the knee entirely on single-threaded boot work, but the practical effect is the same: past ~12, more concurrency buys latency, not throughput. And on a shared vCPU, steal from a noisy neighbor turns a clean 72 ms into a jittery tail. That's the other reason I wanted a dedicated core, a number I can repeat.

The snapshot path hits disk. Pause flushes a sandbox's memory to disk, resume reads it back. My box had a wrinkle: both NVMes were consumed by the OS RAID, so the XFS CubeSandbox wants ran on a loopback image. I assumed that loop device was a tax and left it as a caveat. Then I measured it. It isn't, and the real cause is more interesting. That's the last section.

Containers give you density anywhere. A VM boundary per sandbox is the part you're paying KVM for.

What one user waits as the crowd grows

Fix the batch, sweep concurrency 1 to 50. Per-user latency is flat, ~70 to 140 ms, until concurrency hits 12. That's the thread count. Then it climbs straight. The knee is the whole story. Below your cores everyone gets a fast sandbox. Above it they wait in line.

Per-user spin-up time vs. how many spin at once
each labelled point is the median (p50) wait one user sees · log-scaled concurrency
025751577210291248121624325012 threads0102030405072ms89ms137ms255ms430msms/secconcurrency — how many spin at the same instant (log scale)
p50p95p99throughput

Throughput says the same thing backwards. It caps at ~40 sandboxes/sec at the knee and never gets better. Concurrency past your cores buys latency, not throughput. So the rule is boring. Keep concurrent spin-ups near your core count. Everything else goes behind a warm pool.

A thousand cycles in 23 seconds

Careful what this measures. I ran 100, 200, 500, 1000 create-then-delete cycles at concurrency 40. At any instant ~40 sandboxes exist, not the whole batch. Every run hit 100%. Completed-cycle throughput stayed flat around 40/sec no matter the batch, so wall-clock is linear. A thousand create-delete cycles finished in 23.2 seconds. That's a sustained churn number, not a thousand sandboxes alive at once. Alive-at-once is a different limit, the density section below.

Sustained create-delete: wall-clock and throughput
bars · seconds to run N cycles — line · completed cycles per second
07142127010203040502.7sN=1004.9sN=20012.2sN=50023.2sN=100037/s41/s41/s43/ssec/sec

Per-user latency inside those runs barely moves from 100 to 1000. Latency comes from concurrency, which I held at 40. Not from the total count.

Per-user create latency holds flat from 100 to 1,000
the wait one create sees (percentiles) at concurrency 40 · labels show the median
042384612691692490N=100494N=200508N=500486N=1000ms
p50 (median)p90p95p99

Here's the spread on one full run, all thousand creates bucketed. Most land tight, a thin tail drags out from queuing.

Cold create latency spread, the N=1000 run at concurrency 40
every successful create, bucketed
0306192122p50 486p95 1067p99 1344118 ms2016 mscount
p50 (median)p90p95p99
BatchSuccessWallThroughputp50p95p99
100100%2.7s37/s49011031203
200100%4.9s41/s4949491294
500100%12.2s41/s50811611484
1000100%23.2s43/s48610671344

latency in ms · create-delete · concurrency 40 · 512 MB sandbox-code template

The density wall

Making and freeing a thousand is easy. Holding a thousand alive is where the box pushes back. Ramping create-only, it stopped near 165 concurrent before CubeMaster started returning no more resource. I didn't isolate which check fired. CubeMaster weighs both a declared-memory quota and actual host memory, and this run doesn't tell me which one tripped first.

What I can say is the idle guests were cheap. Across the run, host MemAvailable dropped about 15.5 MB per sandbox created, which fits CubeSandbox's copy-on-write snapshot sharing. Idle sandboxes reserved far more than they touched. That's an idle-density observation, not a claim about loaded guests. Under real workload, density still comes down to RAM and what each guest actually uses.

Declared quota vs. idle-measured memory, per sandbox
512 MB reserved per template vs the host-avail delta per idle sandbox
Declared per templatewhat the scheduler reserves512 MBHost-avail delta, idleper idle sandbox created15.5 MB

The lever for "thousands per node" is the guest's real footprint and the quota math, not raw RAM. My image wouldn't boot under 512 MB, so 165 was my wall on this box. A slimmer guest, or memory overcommit, moves it.

CubeSandbox's own benchmark separates idle overhead from loaded density. Worth reading if you care about the ceiling under load.

Where this fits, next to the managed options

I didn't run the managed products. No numbers for them here. This is only where I'd reach for each.

Vercel's Sandbox runs untrusted, agent-generated code in Firecracker microVMs, with persistent sandbox state and snapshots. If I were already on Vercel, I'd use it and skip running the control plane.

Daytona is a managed programmable sandbox for agent code execution, with container, VM, GPU, and BYOC options. I'd use it when I want a hosted sandbox API and don't want to own the control plane.

Cloud Run runs serverless containers. First-gen instances use gVisor; second-gen run inside a microVM. Services scale to zero by default unless you set minimum instances. I'd use it for a service or a bursty job, not for checkpointing a live coding environment, since it doesn't expose a general pause-resume sandbox API.

For coding agents you want a real filesystem, package installs, and the freedom to run code you didn't write. I don't want model-written code sharing the host kernel. A microVM gives me a harder boundary. Baking Claude Code, Codex, and OpenCode into the template means the sandbox is the agent, ready on resume.

For RL training the win is the snapshot. Thousands of parallel environments, and every rollout needs a clean reset. Cold-booting a fresh env each episode is the slow path. Snapshot a golden environment once, then clone or roll back from that snapshot per rollout, and reset becomes a memory restore instead of a boot. Density decides how many rollouts fit per box, and that's the memory story from two sections up. It's a knob you own on your own hardware.

The trade is the obvious one. Managed means someone else runs the metal and eats the pager. Self-hosted on KVM means you keep the 20 ms path, the snapshot knobs, and the density ceiling. You also keep the RAID that made me put XFS on a loop device.

What each sandbox runs

The benchmark ran a plain code template. But these are full Linux microVMs, so I baked real environments in too. One template with Claude Code, Codex, and OpenCode installed. A desktop variant running Xvfb, fluxbox, noVNC, plus OpenCode's own web UI, all reachable in a browser through the built-in proxy. Building a template is one create-from-image line against a local registry. The snapshot gets taken once the health probe passes. So spinning up a sandbox can mean spinning up a coding agent that's ready to go, not an empty VM.

What makes a warm resume slow (the cache, not the disk)

I blamed the loopback XFS for my slow at-scale resumes. So I measured it, and I was wrong.

fio on the loopback XFS actually beats the direct filesystem on this box: 199k vs 146k 4K random-read IOPS, 8.0 vs 3.3 GB/s sequential, both with O_DIRECT. The loop device isn't a tax. A native NVMe partition wouldn't help.

The real variable is the page cache. A single resume is ~20 ms with the snapshot hot in RAM, ~94 ms cold off disk. At 60 resumes at once it's 33 ms p50 hot and 871 ms p50 cold, because now the box is pulling 60 snapshots back from disk and re-mapping their memory. Same fast disk, 26x apart.

Resume latency: cache-hot vs cache-cold
snapshot resident in page cache vs read back from disk · dashed line is 50 ms
50 ms1 resume · cache-hotsnapshot in RAM20 ms60 at once · hot (p50)pool stays cached33 ms1 resume · cache-coldread from disk94 ms60 at once · cold (p50)60 snapshots off disk871 ms

So a warm pool only stays fast while its snapshots stay resident. That's a RAM and working-set problem, not a faster-disk problem. Size the pool to what fits in cache and the sub-50 ms path holds even under a burst. Let it fall out of cache and every wake pays the disk read again.

In production I'd cap concurrent cold creates around the knee, 12 here, size the paused pool to what stays cached, and remember paused sandboxes hold their quota by default so the pool isn't free. The ~20 ms cached-resume path is real. The 490 ms one is self-inflicted.

Rig: Hetzner AX41. AMD Ryzen 5 3600 (6C/12T), 62 GB, Ubuntu 26.04, KVM, 2×512 GB NVMe RAID1. Method: CubeSandbox v0.5.1 one-click, single node. Latency is the full E2B-compatible POST /sandboxes round-trip. Storage via fio (O_DIRECT), provisioning via the project's cube-bench, density and resume via a stdlib harness. Raw JSON kept locally.