Snapshot Runners for GitHub Actions
Snapshot runners boot a GitHub Actions job from a saved WarpBuild Ubuntu runner disk, so dependencies, working trees and pulled images are already there.
Last verified:
A snapshot runner is a GitHub Actions runner that boots from a disk image captured during an earlier job instead of from the base runner image. WarpBuild supports snapshots on its Cloud Ubuntu runners: append snapshot.enabled=true or snapshot.key=<alias> to the runs-on label, capture the machine mid-workflow with the WarpBuilds/snapshot-save action, and later jobs start with dependencies, checkouts, and pulled container images already on disk.
Three limits shape every decision below. Snapshots run on WarpBuild Cloud Ubuntu runners only. Every snapshot is deleted after 15 days. Snapshot restore and snapshot storage bill as their own line items on top of runner minutes.
Catalog
What a snapshot holds
A snapshot captures the state of the runner VM disk at the moment the snapshot-save action runs. A later job that boots from that snapshot starts from that disk image, so everything written to the filesystem before the save is present:
- installed system packages and language toolchains
- dependency trees such as
node_modules,vendor/, and~/.cargo - the git working tree and its
.gitdirectory in the workspace - Docker images already pulled into the local image store
- compiler and build caches under
$HOME, including Bazel and Gradle output directories
State that lives only in memory is dropped. The machine boots from the snapshot, so no process from the earlier job is running, no port is listening, and no container is up. Any service your steps depend on has to be started again by the job that boots from the snapshot.
/tmp does not persist. The directory is cleaned on reboot and a snapshot boot is a reboot, so anything a build wrote to /tmp is gone on the next run. Keep state you want inside the snapshot under $HOME, inside the workspace, or in another path outside /tmp.
Job identity resets on every run. GitHub hands the job to a fresh runner registration with its own GITHUB_* environment, its own secrets, and its own token. The snapshot supplies the disk; GitHub supplies the job.
A runner that boots from a snapshot exposes the environment variable WARPBUILD_SNAPSHOT_KEY, set to the alias it booted from. Steps read that variable to branch, for example to skip a full dependency install when the tree is already warm, or to print which alias a run started from.
Snapshots are deleted after 15 days. When an alias expires or has never been written, a job with snapshot.key=<alias> still runs: the runner boots from the base image and pays the setup time back on that run.
Where snapshot labels work
WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners. Only the Cloud Ubuntu runners accept snapshots. BYOC runs on AWS, GCP, and Azure, and snapshots are unsupported on all three.
| Runner surface | Snapshot support | Behavior with a snapshot label |
|---|---|---|
| WarpBuild Cloud Ubuntu x64 | Supported | Boots from the snapshot or the base image per the label |
| WarpBuild Cloud Ubuntu ARM64 | Supported | Boots from the snapshot or the base image per the label |
| WarpBuild Cloud macOS | Unsupported | Label silently ignored, job runs normally |
| WarpBuild Cloud Windows | Unsupported | Label silently ignored, job runs normally |
| BYOC on AWS, GCP, or Azure | Unsupported | Label silently ignored, job runs normally |
The silent part matters in practice. A macOS or Windows job carrying snapshot.key=ios-deps produces no warning and no error. It runs at full setup cost every time, and the only symptom is a job duration that never improves. Grep your workflows for snapshot labels on non-Ubuntu runners before you go hunting for a bug.
Snapshot capable runner sizes
Every Cloud Ubuntu size accepts a snapshot label. Rates below are WarpBuild list prices per minute, verified on 2026-08-13.
| Label | Image | vCPU | RAM | Storage | Per minute |
|---|---|---|---|---|---|
warp-ubuntu-latest-x64-2x | Ubuntu 24.04 | 2 | 8 GB | 150GB SSD | $0.004 |
warp-ubuntu-latest-x64-4x | Ubuntu 24.04 | 4 | 16 GB | 150GB SSD | $0.008 |
warp-ubuntu-latest-x64-8x | Ubuntu 24.04 | 8 | 32 GB | 150GB SSD | $0.016 |
warp-ubuntu-latest-x64-16x | Ubuntu 24.04 | 16 | 64 GB | 150GB SSD | $0.032 |
warp-ubuntu-latest-x64-32x | Ubuntu 24.04 | 32 | 128 GB | 150GB SSD | $0.064 |
warp-ubuntu-latest-arm64-2x | Ubuntu 24.04 | 2 | 8 GB | 150GB SSD | $0.003 |
warp-ubuntu-latest-arm64-4x | Ubuntu 24.04 | 4 | 16 GB | 150GB SSD | $0.006 |
warp-ubuntu-latest-arm64-8x | Ubuntu 24.04 | 8 | 32 GB | 150GB SSD | $0.012 |
warp-ubuntu-latest-arm64-16x | Ubuntu 24.04 | 16 | 64 GB | 150GB SSD | $0.024 |
warp-ubuntu-latest-arm64-32x | Ubuntu 24.04 | 32 | 128 GB | 150GB SSD | $0.048 |
Pinned image labels carry the same sizes and the same rates: warp-ubuntu-2604-x64-* and warp-ubuntu-2404-x64-* for Ubuntu 26.04 and 24.04, warp-ubuntu-2204-x64-* for Ubuntu 22.04, plus warp-ubuntu-2604-arm64-* and warp-ubuntu-2404-arm64-* on ARM64. Ubuntu 22.04 ARM64 images were deprecated on March 31, 2025. Pin the image when a snapshot alias has to stay valid across image refreshes, because a base image change and a snapshot built on the older base can drift apart.
One layout detail applies to ARM64: Ubuntu 24.04 ARM64 runners set the work dir to /runner/_work, where GitHub-hosted runners use /home/runner/work/. Absolute paths baked into a snapshot on one architecture do not transfer to the other.
Pricing
Pricing is purely usage based. There is no base subscription fee, no platform fee, and no seat fee. Snapshots add two metered lines on top of runner minutes.
| Line item | Rate | When it bills |
|---|---|---|
| Ubuntu runner time, x64 | $0.004 to $0.064 per minute by size | Every job minute |
| Ubuntu runner time, ARM64 | $0.003 to $0.048 per minute by size | Every job minute |
| Snapshot restore | $0.04 per job | Each job that boots from a snapshot |
| Snapshot storage | $0.025 per snapshot-hour | While a snapshot exists |
| Cache storage | $0.20 per GB-month | While cache entries exist |
| Cache write or restore | $0.0001 per operation | Per cache operation |
| Networking (Tailscale) | Free | Not billed |
The CI Billing report keeps these apart. Its summary cards show total cost, runner cost, snapshot cost, and total jobs for the period, and each table row shows one job execution with its runner label, snapshot usage, billed time, and a cost breakdown split into runner plus snapshot. Filter by snapshot usage to see which repositories are actually paying the restore fee. Full field list is in the WarpBuild reports documentation.
The break-even calculation
A restore costs $0.04 regardless of runner size, so the amount of work a snapshot has to remove before it pays for itself falls as the runner gets bigger. Divide $0.04 by the per-minute rate.
| Label | Rate per minute | Minutes a restore must remove to break even |
|---|---|---|
warp-ubuntu-latest-x64-2x | $0.004 | 10.00 |
warp-ubuntu-latest-x64-4x | $0.008 | 5.00 |
warp-ubuntu-latest-x64-8x | $0.016 | 2.50 |
warp-ubuntu-latest-x64-16x | $0.032 | 1.25 |
warp-ubuntu-latest-x64-32x | $0.064 | 0.63 |
warp-ubuntu-latest-arm64-8x | $0.012 | 3.33 |
Storage is the second term. At $0.025 per snapshot-hour, one alias kept alive costs $0.60 per day and $18.00 per 30-day month. An alias that stops being refreshed is deleted after 15 days, which caps an abandoned alias at $9.00 (15 days times 24 hours times $0.025). Divide that monthly storage figure across the jobs that use the alias: 1,000 jobs against one alias adds $0.018 per job, while 50 jobs against the same alias adds $0.36 per job and usually wipes out the gain.
Worked model: a monorepo test job that pays off
A repository runs 1,000 jobs per month on warp-ubuntu-latest-x64-16x. Setup, which is dependency install plus a cold build cache, takes 8 minutes of a 20-minute job. A snapshot carries that state, so the job runs 12 minutes.
| Line | Without snapshots | With one snapshot alias |
|---|---|---|
| Billed minutes per job | 20.0 | 12.0 |
| Runner cost per job | $0.640 | $0.384 |
| Snapshot restore per job | $0.000 | $0.040 |
| Cost per job | $0.640 | $0.424 |
| 1,000 jobs | $640.00 | $424.00 |
| Snapshot storage, 1 alias, 30 days | $0.00 | $18.00 |
| Monthly total | $640.00 | $442.00 |
For an external reference point, the same 20-minute job on the 16-core GitHub-hosted Linux larger runner (16 vCPU, 64 GB) lists at $0.042 per minute, which is $0.84 per job and $840.00 for 1,000 jobs. GitHub list price from the GitHub Actions minute multipliers reference, checked on 2026-08-13. GitHub-hosted runners have no snapshot equivalent, so that column has no restore or storage line.
Worked model: a small job where snapshots lose
The same arithmetic run on a small runner goes the other way. A repository runs 1,000 jobs per month on warp-ubuntu-latest-x64-2x, and the snapshot removes 4 minutes from a 10-minute job.
| Line | Without snapshots | With one snapshot alias |
|---|---|---|
| Billed minutes per job | 10.0 | 6.0 |
| Runner cost per job | $0.040 | $0.024 |
| Snapshot restore per job | $0.000 | $0.040 |
| Cost per job | $0.040 | $0.064 |
| 1,000 jobs | $40.00 | $64.00 |
| Snapshot storage, 1 alias, 30 days | $0.00 | $18.00 |
| Monthly total | $40.00 | $82.00 |
The 4 minutes removed sit below the 10-minute break-even for a 2x runner, so the restore fee alone reverses the result before storage is counted. Run this arithmetic per workflow with your own numbers from CI Billing before you add a snapshot label. Every rate used here comes from the WarpBuild pricing page and was verified on 2026-08-13.
Signup includes $10 free credits, which covers a few hundred snapshot restores while you measure your own break-even point.
Configuration
Snapshot behavior is selected entirely from the runs-on label. Append the setting to the runner label with a semicolon.
warp-ubuntu-latest-x64-4x;snapshot.enabled=trueturns the feature on. The runner always boots from the base image. Usesnapshot-saveto capture the machine at the point in the workflow you want later jobs to start from.warp-ubuntu-latest-x64-4x;snapshot.key=my-aliasturns the feature on and boots from the existing snapshot for that alias when one is available. When no snapshot exists for the alias, the runner boots from the base image.
The WarpBuilds/snapshot-save action takes three inputs: alias (required, the unique name you boot from later), fail-on-error (optional, default true), and wait-timeout-minutes (optional, default 30). Set fail-on-error: false when a failed capture should leave the pipeline green and let the next run try again.
Full label reference and the security notes live in the WarpBuild snapshot runners documentation.
Use case 1: warm dependency caches
The pattern here separates who writes from who reads. Pushes to main boot clean with snapshot.enabled=true and publish a fresh snapshot. Pull requests boot from that alias with snapshot.key, so the install step finds the dependency tree already in place.
name: test
on:
push:
branches: [main]
pull_request:
jobs:
unit-tests:
runs-on: >-
${{ github.ref == 'refs/heads/main'
&& 'warp-ubuntu-latest-x64-4x;snapshot.enabled=true'
|| 'warp-ubuntu-latest-x64-4x;snapshot.key=web-deps' }}
steps:
- uses: actions/checkout@v5
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm test
- name: Remove credentials before the snapshot
if: github.ref == 'refs/heads/main'
run: rm -rf $HOME/.ssh $HOME/.aws $HOME/.npmrc
- name: Save snapshot
if: github.ref == 'refs/heads/main'
uses: WarpBuilds/snapshot-save@v1
with:
alias: "web-deps"
fail-on-error: false
wait-timeout-minutes: 30The WarpBuild docs recommend git clean -ffdx in the cleanup step. That command removes gitignored paths, which includes node_modules and most dependency directories, so it cancels this use case. Use it when the state you want in the snapshot lives outside the repository, and remove credential files by path when the state you want lives inside it.
A lockfile change on a snapshot boot is handled by the package manager, since pnpm install --frozen-lockfile reconciles the existing tree against the new lockfile rather than starting from nothing. Refresh the alias on main regularly so the drift stays small.
Use case 2: large working trees
Repositories with long histories or large build outputs spend real minutes on actions/checkout and on cold build caches. A snapshot carries both the .git directory and the build output tree, and actions/checkout reuses an existing checkout when the directory already holds the right repository, so the fetch transfers only new objects.
name: build
on: push
jobs:
build:
runs-on: warp-ubuntu-latest-x64-16x;snapshot.key=monorepo-worktree
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Report boot source
run: echo "booted from ${WARPBUILD_SNAPSHOT_KEY:-base image}"
- name: Build
run: bazel build //...
- name: Remove credentials before the snapshot
run: rm -rf $HOME/.ssh $HOME/.aws
- name: Save snapshot
uses: WarpBuilds/snapshot-save@v1
with:
alias: "monorepo-worktree"
fail-on-error: false
wait-timeout-minutes: 45Every run here refreshes the alias, so each snapshot builds on the previous one. That keeps the working tree and the Bazel output base warm across the branch. Sizing and remote cache setup for this shape are covered in the Bazel on GitHub Actions solution page.
Raise wait-timeout-minutes when the disk is large. The default of 30 minutes is generous for a dependency tree and tight for a snapshot holding tens of gigabytes of build output.
Use case 3: prepared containers
Integration suites that pull database, queue, and fixture images spend minutes on registry transfers. Images pulled into the local Docker store live on disk, so they survive in the snapshot. Running containers do not, because the machine reboots, so bring services up in every job.
name: integration
on: pull_request
jobs:
integration:
runs-on: warp-ubuntu-latest-x64-8x;snapshot.key=integration-images
steps:
- uses: actions/checkout@v5
- name: Pull service images
run: docker compose -f docker-compose.ci.yml pull --quiet
- name: Start services
run: docker compose -f docker-compose.ci.yml up -d --wait
- name: Run integration tests
run: ./scripts/integration-tests.sh
- name: Stop services before the snapshot
run: docker compose -f docker-compose.ci.yml down
- name: Remove credentials before the snapshot
run: rm -rf $HOME/.ssh $HOME/.aws $HOME/.docker/config.json
- name: Save snapshot
uses: WarpBuilds/snapshot-save@v1
with:
alias: "integration-images"
fail-on-error: false
wait-timeout-minutes: 30The docker pull step stays in the workflow on purpose. On a snapshot boot it confirms the digests already present and downloads only what changed.
Cleanup and security
Run a cleanup step before every save. Registry credentials in $HOME/.docker/config.json, cloud credentials in $HOME/.aws, and SSH keys in $HOME/.ssh all sit on the disk the snapshot captures.
Two exposure paths make this concrete. On a public repository, anyone who can open a pull request can reference the alias in a workflow run and boot a machine from your snapshot. On a private repository, WarpBuild provisions runners at the organization level and GitHub may hand a runner intended for snapshot jobs to a different job in the organization, which puts snapshot contents in front of other members of the org.
When to stay on stateless runners
Snapshots earn their fee under narrow conditions. Skip them when any of these hold:
- Setup is shorter than the break-even. Below 10 minutes of removable work on a 2x runner or 5 minutes on a 4x runner, the restore fee costs more than it saves.
- A cache action already covers the state. Dependency directories restore well from the WarpBuild cache with
WarpBuilds/cache@v1, which bills $0.0001 per operation and $0.20 per GB-month against the $0.04 restore fee. Start there and read the persistent caches for GitHub Actions guide before adding a snapshot alias. Teams hitting entry size ceilings should read the GitHub Actions cache size limit guide first. - The job must start from a known clean disk. Release builds, compliance jobs, and anything that signs artifacts should boot from the base image.
- Wall clock matters more than minutes. Snapshot runners take 45 to 60 seconds to boot, which is slower than a default runner boot.
- The workflow runs infrequently. Snapshots expire after 15 days, and a low-volume alias spreads $18.00 of monthly storage across too few jobs.
- The runner is macOS, Windows, or BYOC. The label is silently ignored on those surfaces.
Automation
Wire a scheduled job to delete aliases whose workflow was removed, so storage stops billing before the 15-day expiry does it for you. Definitions of the concept and its neighbors sit on what are snapshot runners.
FAQ
Do snapshot runners work on Windows, macOS, or BYOC runners?
No. Snapshot runners are supported only on WarpBuild Cloud Ubuntu runners. If you put snapshot.enabled=true or snapshot.key=<alias> on a BYOC, Windows, or macOS runner label, the label is silently ignored and the job runs normally without snapshot functionality.
What is the difference between snapshot.enabled=true and snapshot.key=alias?
snapshot.enabled=true turns the feature on and always boots the runner from the base image, so it is the label for the job that produces a clean snapshot. snapshot.key=<alias> also turns the feature on and boots from the existing snapshot for that alias when one exists, falling back to the base image when it does not.
Does /tmp persist across a snapshot?
No. The /tmp directory is cleaned on reboot and a snapshot boot is a reboot, so nothing written to /tmp survives. Keep state you want in the snapshot under $HOME, inside the workspace, or in another path outside /tmp.
How long does a WarpBuild snapshot live and what does it cost to keep?
Snapshots are deleted after 15 days. Storage bills at $0.025 per snapshot-hour, which is $0.60 per day and $18.00 per 30-day month for one alias kept alive, and a restore bills $0.04 for each job that boots from a snapshot. Both lines appear separately from runner cost on the CI Billing report and on the pricing page.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.