How Do I Cache Dependencies in a Sandbox With a Named Snapshot?
Updated October 27, 2026 .md
Short answer: Install dependencies once in a sandbox, save it with sbx snapshot under a name that includes a hash of your lockfiles, and start each job with sbx new --from that name. On prod this replaced about 51 seconds of npm, uv and pip installs with a hash check. Rebuild the snapshot when the hash changes.
Results#
We ran this on prod on 2026-10-27 on default sandboxes (4 vCPU, 8 GB) in eu-central. Three runs per row; the median is in bold.
| Install | Fresh sandbox, empty cache | From the snapshot, cache only (node_modules / venv deleted) |
From the snapshot, as saved |
|---|---|---|---|
npm ci, Next.js app, 1,455 packages |
23.4, 23.8, 25.3 s | 17.5, 17.9, 18.3 s | 0 s (skipped) |
uv sync --frozen, FastAPI app, 77 packages |
3.1, 1.9, 1.9 s | 0.1, 0.0, 0.0 s | 0 s (skipped) |
pip install -r requirements.txt, same FastAPI deps |
25.5, 25.5, 23.2 s | 18.1, 18.4, 18.5 s | 0 s (skipped) |
| Total | 51.2 s | 36.3 s | 0.00 s hash check |
The projects were two real public repos at their current commits: mckaywrigley/chatbot-ui (Next.js 14, package-lock.json, commit 81328b6) and fastapi/full-stack-fastapi-template (uv.lock, commit f4bcfab). The pip row installs the same locked dependencies, exported with uv export, into a plain python3 -m venv.
Creating the sandbox cost the same either way: sbx new and sbx new --from each reported ready in 3.2 seconds (one run each).
What the numbers say:
- The big win is skipping the install, not the warm cache. With
node_modulesand the venv already on disk, there is nothing to do. A warm npm or pip cache alone saved only about a quarter of the time, because both still unpack and link every package. - uv is already fast. A cold
uv synctook under 2 seconds once its Python was present (run 1 also fetched the project's managed CPython 3.14). The snapshot still matters for uv when you use it to skip a slow toolchain or a large wheel set, but the per-job saving here was small. - Measure your own project. These are network-bound installs on one day. A project with native builds (node-gyp, Rust wheels, source-built Python packages) gains more.
Step 1: name the snapshot after your lockfiles#
A named snapshot has no versions: saving again under the same name replaces it. So put the version in the name. Hash the lockfiles, and the name changes exactly when your dependencies do:
cat web/package-lock.json api/uv.lock | sha256sum | cut -c1-12312bbcdb2bceThe snapshot is then chatbot-deps-312bbcdb2bce. Names are 1 to 63 lowercase letters, digits or dashes, so keep the prefix short.
Step 2: a script that reuses or rebuilds it#
deps-snapshot.sh prints the snapshot name for the current lockfiles. If no snapshot has that name, it builds one in a throwaway sandbox, saves it, deletes the builder, and removes older snapshots with the same prefix. It needs sbx and jq on the machine that runs it (your laptop or CI runner).
#!/usr/bin/env bash
# Print the name of a named snapshot whose dependencies match these lockfiles.
# Build it first if there is none.
# Usage: deps-snapshot.sh <prefix> '<install command>' <lockfile>...
set -euo pipefail
prefix=$1 install=$2; shift 2
# 1. Name the snapshot after a hash of the lockfiles.
hash=$(cat "$@" | sha256sum | cut -c1-12)
name="$prefix-$hash"
# 2. Reuse it if it exists.
if sbx snapshots --json | jq -e --arg n "$name" '.named[] | select(.name == $n)' >/dev/null; then
echo "$name"; exit 0
fi
# 3. Build it: fresh sandbox, install, save, delete the builder.
id=$(sbx new --name "build-$name" --type default --json | jq -r 'select(.event == "ready") | .id')
trap 'sbx delete "$id" --yes >/dev/null' EXIT
sbx exec "$id" --timeout 600 "$install && echo $hash > ~/.deps-hash" >&2
sbx snapshot "$id" "$name" >&2
# 4. Remove older snapshots with the same prefix.
sbx snapshots --json |
jq -r --arg p "$prefix-" --arg n "$name" '.named[] | select((.name | startswith($p)) and .name != $n) | .name' |
while read -r old; do sbx snapshot rm "$old" >&2; done
echo "$name"The install command for our two projects, in install.sh. It runs as user in /home/user, so the npm, pip and uv caches land in the home directory and are saved with the disk:
set -e
git clone -q --depth 1 https://github.com/mckaywrigley/chatbot-ui web
git clone -q --depth 1 https://github.com/fastapi/full-stack-fastapi-template api
cd ~/web && npm ci --no-audit --no-fund --loglevel=error
cd ~/api && uv sync --frozen --quiet
uv export --frozen --no-hashes --no-emit-workspace --quiet > requirements.txt
python3 -m venv pipvenv && pipvenv/bin/pip install -q -r requirements.txtRun it from a checkout that has the same lockfiles:
time ./deps-snapshot.sh chatbot-deps "$(cat install.sh)" web/package-lock.json api/uv.lockThe first run built the snapshot (output trimmed):
added 1455 packages in 24s
{"ok":true,"type":"snapshot.named.info","snapshot":{"name":"chatbot-deps-312bbcdb2bce",…,"status":"ready","type":"default",…}}
chatbot-deps-312bbcdb2bce
real 1m1.016sThe second run found it and returned at once:
chatbot-deps-312bbcdb2bce
real 0m0.133sThe builder sandbox was gone afterwards (sbx info returned not_found), because the trap deletes it even if the install fails.
Step 3: start every job from it#
name=$(./deps-snapshot.sh chatbot-deps "$(cat install.sh)" web/package-lock.json api/uv.lock)
sbx new --from "$name"The new sandbox takes the snapshot's size (default here) unless you pass --type. Its dependencies work straight away:
sbx exec sb_z6j5c9bgpc 'cd ~/web && npx next --version; cd ~/api && uv run --frozen python -c "import fastapi, sqlmodel; print(\"fastapi\", fastapi.__version__)"'Next.js v14.1.0
fastapi 0.141.1Step 4: check the hash inside the sandbox#
The job may check out a newer commit than the one in the snapshot. Put a guard at the start of the job: skip the install when the lockfiles still match the hash saved at build time, and fall back to an install from the warm cache when they don't.
#!/usr/bin/env bash
# deps-check.sh: run at the start of every job in a sandbox made with --from.
set -euo pipefail
want=$(cat ~/web/package-lock.json ~/api/uv.lock | sha256sum | cut -c1-12)
if [ "$want" = "$(cat ~/.deps-hash 2>/dev/null)" ]; then
echo "deps match snapshot ($want), skipping install"
else
echo "lockfiles changed, installing from the warm cache"
(cd ~/web && npm ci --prefer-offline --no-audit --no-fund)
(cd ~/api && uv sync --frozen)
echo "$want" > ~/.deps-hash
fiWhen the lockfiles matched (three runs):
deps match snapshot (312bbcdb2bce), skipping install
0.00 sWhen we made the saved hash stale, the fallback ran and took 21.3 seconds (one run), still faster than a cold install because the caches were in the snapshot:
lockfiles changed, installing from the warm cache
…
added 1455 packages in 21s
Checked 77 packages in 0.74ms
21.3 sIf the fallback runs often, your snapshot is out of date. Run deps-snapshot.sh again from CI on the main branch so the next job gets a fresh one.
When to rebuild#
| Change | Rebuild? |
|---|---|
A lockfile changed (package-lock.json, uv.lock, requirements.txt, pnpm-lock.yaml) |
Yes. The hash changes, so the script builds a new name. |
| Only source code changed | No. Pull the new code in the job; node_modules and the venv still match. |
| Your install step changed (a new apt package, another Node or Python version) | Yes. Pass install.sh as one more file to hash, so editing it also changes the name. |
| The snapshot is old but the lockfiles match | Only if the install step fetches something no lockfile pins, such as apt-get upgrade. |
What it costs#
A named snapshot has no separate charge. What it stores counts toward your plan's included storage, together with stopped sandboxes: 10 GB on the free trial. Storage is measured as the data beyond the shared OS image. In our build, node_modules was 1.4 GB, ~/.npm 277 MB, the uv cache 253 MB and the pip cache 95 MB. That is why the script deletes older snapshots with the same prefix. See Billing for the other plans.
FAQ#
Should the snapshot hold node_modules or only the npm cache?#
Both. In our runs the installed node_modules saved all 23.8 seconds, and the cache alone saved about 6. The cache is the fallback for when the lockfile changes between snapshot builds.
Does the snapshot keep running processes?#
No. A named snapshot saves the disk, so a sandbox started from it boots fresh. Enabled systemd services start; anything else you start in the job.
Can I put API keys in the dependency snapshot?#
Keep them out. A named snapshot saves the whole disk, including any .npmrc or credentials file the build step wrote. Pass secrets at job time with --environment next to --from.
Why hash the lockfile and not package.json?#
The lockfile pins every resolved version, so its hash changes exactly when the installed tree would. package.json or pyproject.toml can stay the same while resolved versions move.
Is a fork faster than --from for this?#
Both copy a disk, so the dependencies come across either way. A fork needs its source sandbox to still exist; a named snapshot doesn't, which is why the script deletes the builder.
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.