# Docker Image Pull Time in a Sandbox, Measured

> 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.

Source: https://sandboxer.com/guides/docker-image-pull-time-in-a-sandbox. Updated 2026-10-27.

**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.20` is 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:22` came 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 pull` of an image you already have still checks the registry (0.9 s) but downloads nothing. Skip the pull and `docker run` starts a container in 0.2 s.
- **Images survive a stop.** After `sbx stop --wait` and `sbx resume` (memory restored), `docker images` still listed `node:22` and 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:

```text
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 s
```

Scale 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](https://docs.docker.com/docker-hub/usage/) (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](https://docs.docker.com/docker-hub/usage/pulls/); we ran it in a fresh sandbox before pulling anything:

```bash
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 ratelimit
```

```text
docker-ratelimit-source: 2a01:4f9:…::
ratelimit-limit: 100;w=3600
ratelimit-remaining: 93;w=3600
```

The 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 stop` and `sbx resume`.
- **Start new sandboxes from a named snapshot** that already has your images. Pull once, `sbx snapshot <id> <name>`, then `sbx new --from <name>`. The walkthrough is in [Docker and nested KVM inside an agent sandbox](/guides/docker-and-kvm-inside-an-agent-sandbox), and what a snapshot keeps is in the [snapshots docs](/docs/snapshots).

## The exact commands

This script ran inside each sandbox, once per run (`bash bench.sh 1`, `2`, `3`):

```bash
#!/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
done
```

From our machine, per size:

```bash
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" --yes
```

The `small` sandbox printed:

```text
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:08Z
```

Keep `--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](/guides/cpu-performance-by-sandbox-size) and [disk throughput by sandbox size](/guides/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

- [Docker and nested KVM inside an agent sandbox](/guides/docker-and-kvm-inside-an-agent-sandbox)
- [Run Docker Compose in a sandbox with a URL per service](/guides/run-docker-compose-in-a-sandbox-with-a-url-per-service)
- [How fast does a cloud sandbox start, resume and fork? Measured](/guides/how-fast-does-a-cloud-sandbox-start-resume-and-fork)
- [Snapshots](/docs/snapshots)
