Docker Image Pull Time in a Sandbox, Measured
Updated October 27, 2026 .md
Short answer: Measured on 2026-10-27 in eu-central, 3 runs per size. A cold docker pull of node:22 (408 MB compressed) took a median 15.7 to 16.0 seconds, python:3.12-slim 3.7 to 3.9 s and alpine:3.20 about 1.6 s. Sandbox size made no difference. An image already on disk, also after a resume, starts in 0.2 s.
Conditions#
| Date and time | 2026-10-27. small 11:46 to 11:47 UTC, default 11:59 to 12:00 UTC, large 12:00 to 12:02 UTC, the extra tests below 12:03 to 12:04 UTC |
| Region | eu-central |
| Sizes | small (2 vCPU, 4 GB), default (4 vCPU, 8 GB), large (8 vCPU, 16 GB) |
| Docker | 29.9.0, overlay2 storage driver, no registry mirror configured, BuildKit on (from docker info and /etc/docker/daemon.json) |
| Registry | Docker Hub, anonymous pulls of official images, linux/amd64 |
| Runs | One fresh sandbox per size, 3 runs of each image, with docker system prune -af before every pull so each one started from an empty image store. Each sandbox was deleted after its runs |
| Neighbors | Other sandboxes on the account were running at the same time, so the host and its network may not have been idle |
These are measurements of Sandboxer only. We didn't test any other service. Results vary by host, by what else is using the network, and by Docker Hub itself over time.
The images#
A fresh sandbox has Docker running and no images (docker images was empty on all three sizes). We pulled three official images of very different sizes. Compressed size is the sum of the linux/amd64 layer sizes in the registry manifest; on-disk size is what docker image inspect reports after the pull.
| Image | Layers | Compressed | On disk |
|---|---|---|---|
alpine:3.20 |
1 | 3.6 MB | 8 MB |
python:3.12-slim |
4 | 43.5 MB | 120 MB |
node:22 |
8 | 408.4 MB | 1,131 MB |
Results#
Wall-clock seconds for docker pull -q <image> from an empty image store. Median of 3 runs, with the quickest and slowest run.
| Image | small |
default |
large |
|---|---|---|---|
alpine:3.20 |
1.59 (1.59 to 1.61) | 1.60 (1.58 to 2.29) | 1.64 (1.52 to 1.90) |
python:3.12-slim |
3.67 (3.51 to 3.73) | 3.85 (3.43 to 4.02) | 3.72 (3.46 to 3.92) |
node:22 |
15.65 (15.42 to 16.11) | 15.95 (15.51 to 16.07) | 15.67 (15.58 to 15.79) |
With the image already present, in a small sandbox:
| Step | Run 1 | Run 2 | Run 3 |
|---|---|---|---|
docker pull node:22 (image already there) |
0.89 s | 0.87 s | 0.86 s |
docker run --rm node:22 node -v |
0.18 s | 0.18 s | 0.20 s |
The same docker run, after sbx stop --wait and sbx resume |
0.21 s | 0.18 s | 0.17 s |
How to read the numbers#
- Size doesn't buy a faster pull. The three sizes landed within 2% of each other on
node:22, well inside the spread between runs. Pick the size for what the containers do once they run. - Small images are mostly round trips.
alpine:3.20is 3.6 MB, yet its pull took 1.6 seconds: token, manifest index, manifest, config and one layer, each a separate request to Docker Hub. At that rate, ten small images pulled one after another would take about 16 seconds, mostly waiting. - Large images are mostly unpacking.
node:22came in at about 26 MB/s of compressed data end to end. Downloading its biggest layer on its own was more than three times faster than that (next section), so most of the 15.7 seconds goes into decompressing and writing 1.1 GB of files. - A present image is free. A
docker pullof an image you already have still checks the registry (0.9 s) but downloads nothing. Skip the pull anddocker runstarts a container in 0.2 s. - Images survive a stop. After
sbx stop --waitandsbx resume(memory restored),docker imagesstill listednode:22and the container started in the same 0.2 s. A stop snapshot keeps the whole disk, Docker images included.
Where the time goes in a large pull#
To split network from unpacking, we downloaded the largest node:22 layer (211.8 MB compressed) with curl straight from Docker Hub's blob endpoint, then timed gunzip on it, 3 times on a fresh small sandbox:
largest layer: 211.8 MB compressed
run=1 download=2.22 s (95 MB/s) gunzip=2.38 s
run=2 download=2.37 s (89 MB/s) gunzip=2.30 s
run=3 download=2.43 s (87 MB/s) gunzip=2.20 sScale that to the whole image and the download is roughly 4.5 seconds and the decompression roughly 4.5 seconds. The remaining 6 seconds or so is most likely Docker writing 1.1 GB of files into its layer store and registering the image; we didn't time that step on its own. gunzip works on one core, which is one reason more vCPUs didn't help.
We also pulled all three images at once (three docker pull commands in the background, then wait) on the same sandbox. The medians of the three single pulls add up to 20.9 seconds; the parallel pull took 16.33, 21.69 and 19.80 seconds, a median of 19.80. Parallel pulls saved about a second, and run to run varied more.
Docker Hub's pull limit is shared per /64#
A sandbox reaches Docker Hub over IPv6 first: registry-1.docker.io resolved to IPv6 addresses before IPv4. Docker Hub's usage docs (read 2026-10-27) say anonymous pull limits apply per IPv4 address or per IPv6 /64 subnet. Docker documents a check for your remaining pulls; we ran it in a fresh sandbox before pulling anything:
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
curl -s --head -H "Authorization: Bearer $TOKEN" \
https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest | grep -i ratelimitdocker-ratelimit-source: 2a01:4f9:…::
ratelimit-limit: 100;w=3600
ratelimit-remaining: 93;w=3600The source is a /64, not the sandbox's own address, and 7 pulls had already been counted before this sandbox pulled anything, so other sandboxes on the same host share that budget. Over IPv4 (curl -4), the source was the host's shared IPv4 address, so that limit is shared too. If a CI job or an agent pulls many images, sign in with your own Docker account inside the sandbox (docker login) so pulls count against your account instead, or keep the images on disk so you don't pull at all.
Avoid the pull: keep images on disk#
The quickest pull is the one you skip. Two ways that the numbers above support:
- Stop instead of delete. Images, volumes and the build cache stay on the sandbox's disk through
sbx stopandsbx resume. - Start new sandboxes from a named snapshot that already has your images. Pull once,
sbx snapshot <id> <name>, thensbx new --from <name>. The walkthrough is in Docker and nested KVM inside an agent sandbox, and what a snapshot keeps is in the snapshots docs.
The exact commands#
This script ran inside each sandbox, once per run (bash bench.sh 1, 2, 3):
#!/bin/bash
# usage: bench.sh <run-number>
for img in alpine:3.20 python:3.12-slim node:22; do
docker system prune -af >/dev/null 2>&1
t0=$(date +%s.%N)
docker pull -q "$img" >/dev/null 2>/tmp/pull.err; rc=$?
t1=$(date +%s.%N)
size=$(docker image inspect -f '{{.Size}}' "$img" 2>/dev/null)
printf 'run=%s img=%s rc=%s secs=%.2f size_mb=%.0f at=%s\n' "$1" "$img" "$rc" \
"$(python3 -c "print($t1-$t0)")" "$(python3 -c "print(${size:-0}/1e6)")" "$(date -u +%TZ)"
[ $rc -ne 0 ] && cat /tmp/pull.err
doneFrom our machine, per size:
id=$(sbx new --type small --name docker-pull-1 --no-env --ttl 3600 --json \
| jq -r 'select(.event=="ready") | .id' | tail -1)
sbx exec "$id" "echo $(base64 -w0 bench.sh) | base64 -d > /home/user/bench.sh"
for r in 1 2 3; do sbx exec "$id" --timeout 600 "bash /home/user/bench.sh $r"; done
sbx delete "$id" --yesThe small sandbox printed:
run=1 img=alpine:3.20 rc=0 secs=1.61 size_mb=8 at=11:46:03Z
run=1 img=python:3.12-slim rc=0 secs=3.73 size_mb=120 at=11:46:07Z
run=1 img=node:22 rc=0 secs=16.11 size_mb=1131 at=11:46:23Z
run=2 img=alpine:3.20 rc=0 secs=1.59 size_mb=8 at=11:46:26Z
run=2 img=python:3.12-slim rc=0 secs=3.67 size_mb=120 at=11:46:30Z
run=2 img=node:22 rc=0 secs=15.42 size_mb=1131 at=11:46:45Z
run=3 img=alpine:3.20 rc=0 secs=1.59 size_mb=8 at=11:46:49Z
run=3 img=python:3.12-slim rc=0 secs=3.51 size_mb=120 at=11:46:52Z
run=3 img=node:22 rc=0 secs=15.65 size_mb=1131 at=11:47:08ZKeep --timeout on sbx exec: it defaults to 30 seconds, and a cold pull of a bigger image than node:22 can pass that. For pulls that take minutes, run them with sbx exec --detach and poll. We got the compressed sizes from the registry manifest with docker buildx imagetools inspect --raw, picking the linux/amd64 entry.
What we left out#
xlarge. Not tested; it needs the $100/month plan or higher.- Other registries (GHCR, ECR, Quay) and private images. Their CDNs and auth differ.
docker build. A build adds base-image pulls plus your own steps; see CPU performance by sandbox size and disk throughput by sandbox size for the parts a build leans on.
FAQ#
Does a bigger sandbox pull images faster?#
Not in our runs. node:22 took a median 15.65 s on small, 15.95 s on default and 15.67 s on large. The time goes to network round trips and single-stream decompression, which extra vCPUs don't speed up.
Do pulled images survive stopping the sandbox?#
Yes. After sbx stop --wait and sbx resume, node:22 was still in docker images and docker run started in 0.2 s with no pull. Images are on the disk, and a stop snapshot keeps the disk.
Is there a registry mirror or pull-through cache?#
Not by default. docker info showed no registry mirrors and /etc/docker/daemon.json set only BuildKit, the log driver and the storage driver. Every cold pull went to Docker Hub.
Will I hit Docker Hub's rate limit?#
You can if you pull a lot anonymously. The limit was counted per IPv6 /64 shared with other sandboxes on the same host, and 7 of 100 pulls were already used when we checked. Run docker login with your own Docker account or keep images on disk.
Will my numbers match these?#
Not exactly. Our three runs of one image differed by up to 45% for alpine:3.20 and 5% for node:22. Results vary by host, by network load and by Docker Hub over time.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.