What Are Snapshot Runners?

A snapshot runner boots a GitHub Actions job from a saved image of an earlier run's VM instead of a clean base image. What persists, what resets, what it costs.

Last verified:

A snapshot runner is a GitHub Actions runner that boots from a saved image of a previous run's virtual machine instead of from a clean base image, so the packages, dependency directories, container images, and build outputs an earlier run left on disk are already present when the first step executes. WarpBuild implements snapshot runners on its Cloud Ubuntu runners: you append snapshot.enabled=true or snapshot.key=<alias> to the runs-on label, capture the machine with the WarpBuilds/snapshot-save action, and later jobs that request the same alias start from that captured disk.

Answer

Every job on an ephemeral runner starts from an identical base image. That is what makes GitHub Actions reproducible, and it is also why the same forty minutes of apt-get install, npm ci, docker pull, and cold compilation get repeated on every pull request in a busy repository. A snapshot runner changes the starting point of that job without changing the isolation model.

Three properties define the mechanism:

  • The unit is the whole runner disk. A snapshot is taken at whatever point in the workflow you call the save action, and it captures the machine as it stood at that moment, including files no cache action was ever configured to track.
  • A snapshot is addressed by an alias you choose. The alias is a string such as web-app-main. A job asks for it by putting snapshot.key=web-app-main in its runs-on label.
  • The job stays ephemeral. WarpBuild allocates a fresh virtual machine per job and destroys it when the workflow completes. The saved image is what survives, and it survives for 15 days.

That third property is what separates a snapshot runner from a persistent self-hosted runner. A persistent runner keeps one machine registered and hands it job after job, accumulating state that nobody chose and that no branch can reset. A snapshot runner builds a new machine for every job from a disk image you named and can rebuild on demand.

The distinction from caching is just as concrete. A cache action restores named paths into a clean machine. Restoring 3 GB of node_modules still leaves the apt packages uninstalled, the Docker daemon's local image store empty, the Rust target directory partial, and every compiled artifact outside the cached paths missing. A snapshot carries the disk, so whatever a previous run installed anywhere on that disk is present at step one. The WarpBuild snapshot runners documentation is the reference for the exact behavior, and the persistent caches guide covers the cases where a cache is the better tool.

Platform scope is the first thing to check before designing around this. Snapshot runners apply to the Ubuntu part of that catalog, described in the cloud runners documentation.

Detail

What carries over and what resets

A snapshot holds the runner VM's disk at the instant snapshot-save ran. Installed system packages, cloned repositories, dependency directories, container images pulled into the local Docker store, compiler caches, and generated build outputs all come back with the machine.

Two behaviors need planning around.

/tmp does not persist. The directory is cleaned on reboots, and booting from a snapshot is a boot, so anything a job wrote to /tmp before the save is gone on restore. Build tooling that stages artifacts in /tmp needs a different path if you want those artifacts in the snapshot.

Secrets on disk do persist. Credentials written by a login step, an SSH key, an AWS profile, or a token file left in a working directory are part of the disk and therefore part of the image. This is the reason the documentation puts a cleanup step immediately before every save, and the reason the security section below matters more than the cost section.

One signal comes back with the machine: a runner created from a snapshot has the environment variable WARPBUILD_SNAPSHOT_KEY set to the alias it booted from. That variable is how a workflow knows whether it is on a warm machine and can skip the setup steps that a warm machine no longer needs.

The two snapshot labels

Snapshot behavior is requested through dynamic labels appended to the runner label with a semicolon. There are two, and they do different jobs:

  • snapshot.enabled=true turns the feature on and always boots from the base image. Use it on the workflow that produces the snapshot, so that the image you capture is built from a known clean starting point.
  • snapshot.key=<alias> turns the feature on and boots from the existing snapshot for that alias when one exists. When no snapshot exists yet for the alias, the runner boots from the base image and the job proceeds normally.

Both forms are written inline in runs-on, for example warp-ubuntu-latest-x64-4x;snapshot.key=web-app-main. If either label is applied to a runner type that does not support snapshots, the label is silently ignored and the job runs without snapshot behavior. There is no error and no warning in the job log, so a typo in the runner label is indistinguishable from a working configuration until you check whether WARPBUILD_SNAPSHOT_KEY was set.

Capturing a snapshot

The capture step is the WarpBuilds/snapshot-save action, published at github.com/WarpBuilds/snapshot-save. Place it at the point in the workflow where the disk holds the state you want the next run to start from, which is usually after dependencies are installed and the build has warmed the compiler caches, and always after credentials have been removed.

InputRequiredDefaultPurpose
aliasYesnoneUnique name for the snapshot, used later in snapshot.key
fail-on-errorNotrueFail the job when snapshot creation errors
wait-timeout-minutesNo30Maximum minutes to wait for the snapshot to be created

Setting fail-on-error: false is the right choice when the snapshot is an optimization and a failed capture should leave the pipeline green. Raising wait-timeout-minutes above the default matters for large disks, where the capture takes longer than half an hour.

Runners that accept snapshot labels

Snapshot runners are supported only on WarpBuild Cloud Ubuntu runners. The table below lists the Ubuntu 24.04 labels and their published rates, taken from the cloud runners documentation and the pricing page, checked on 2026-08-13.

Runner labelOSvCPURAMStoragePrice per minute
warp-ubuntu-latest-x64-2xUbuntu 24.0428 GB150GB SSD$0.004
warp-ubuntu-latest-x64-4xUbuntu 24.04416 GB150GB SSD$0.008
warp-ubuntu-latest-x64-8xUbuntu 24.04832 GB150GB SSD$0.016
warp-ubuntu-latest-x64-16xUbuntu 24.041664 GB150GB SSD$0.032
warp-ubuntu-latest-x64-32xUbuntu 24.0432128 GB150GB SSD$0.064
warp-ubuntu-latest-arm64-2xUbuntu 24.0428 GB150GB SSD$0.003
warp-ubuntu-latest-arm64-4xUbuntu 24.04416 GB150GB SSD$0.006
warp-ubuntu-latest-arm64-8xUbuntu 24.04832 GB150GB SSD$0.012
warp-ubuntu-latest-arm64-16xUbuntu 24.041664 GB150GB SSD$0.024
warp-ubuntu-latest-arm64-32xUbuntu 24.0432128 GB150GB SSD$0.048

The Ubuntu 26.04 labels carry the same sizes and rates for both x64 and ARM64, under warp-ubuntu-2604-*. Ubuntu 22.04 carries the same x64 sizes and rates under warp-ubuntu-2204-*; WarpBuild deprecated the Ubuntu 22.04 ARM64 images on March 31, 2025, so the 22.04 label is x64 only. Each latest label also has a pinned alias, so warp-ubuntu-latest-x64-4x and warp-ubuntu-2404-x64-4x route to the same machine shape. A snapshot boots on the runner shape you request in runs-on, so keep the size stable across the workflow that saves the snapshot and the workflows that restore it.

A workflow that saves on main and restores on pull requests

This is the shape most teams want. The default branch always boots clean and republishes the snapshot, so the image never drifts. Pull request runs boot from the published image and skip the setup work it already contains.

name: build
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: >-
      ${{ github.ref == 'refs/heads/main'
        && 'warp-ubuntu-latest-x64-4x;snapshot.enabled=true'
        || 'warp-ubuntu-latest-x64-4x;snapshot.key=web-app-main' }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Install system packages
        run: |
          if [ -z "$WARPBUILD_SNAPSHOT_KEY" ]; then
            sudo apt-get update
            sudo apt-get install -y libvips-dev protobuf-compiler
          else
            echo "booted from snapshot $WARPBUILD_SNAPSHOT_KEY"
          fi

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Test
        run: npm test

      - name: Cleanup credentials
        if: github.ref == 'refs/heads/main'
        run: |
          rm -rf $HOME/.ssh $HOME/.aws
          git clean -ffdx

      - name: Save snapshot
        if: github.ref == 'refs/heads/main'
        uses: WarpBuilds/snapshot-save@v1
        with:
          alias: "web-app-main"
          fail-on-error: true
          wait-timeout-minutes: 60

The npm ci step is left unguarded on purpose. It reconciles the lockfile against the node_modules tree that came out of the snapshot, which is fast when the tree is already correct and correct when the lockfile moved on since the image was captured. Guard the steps whose work is expensive and idempotent, such as system package installation and toolchain downloads, and leave the steps that enforce correctness running every time.

The second shape is a single alias that updates itself on every run, with no separate producing branch:

jobs:
  integration:
    runs-on: warp-ubuntu-latest-x64-8x;snapshot.key=integration-fixtures
    steps:
      - uses: actions/checkout@v5

      - name: Seed fixtures
        run: ./scripts/seed-fixtures.sh

      - name: Integration tests
        run: ./scripts/integration.sh

      - name: Cleanup credentials
        run: |
          rm -rf $HOME/.ssh $HOME/.aws
          git clean -ffdx

      - uses: WarpBuilds/snapshot-save@v1
        with:
          alias: "integration-fixtures"
          fail-on-error: false

Each run boots from the previous run's disk and writes a new one, so state compounds run over run. That is useful for fixture data and large downloaded corpora. It also means a bad run can poison the alias, and the fix is to change the alias string, which sends the next job back to the base image.

Cleanup and security

Two exposure paths deserve explicit handling before a snapshot alias goes into a workflow file.

On public repositories, a pull request workflow can name any alias in runs-on. A contributor who reads your workflow file knows the alias, so any credential left in a snapshot is reachable by anyone who can open a pull request. Cloud credentials, registry tokens, and signing keys belong outside snapshots on public repositories.

On private repositories, WarpBuild provisions runners at the organization level, and GitHub may allocate a runner intended for a snapshot job to a different job in the same organization. A snapshot that carries secrets can therefore surface them to other users in the organization.

The mitigation is the same in both cases: remove credentials before the save step runs.

rm -rf $HOME/.ssh $HOME/.aws
git clean -ffdx

git clean -ffdx removes untracked files from the repository working tree, including directories and files ignored by .gitignore. It is the blunt version of the cleanup and it will also delete build output you may have wanted in the snapshot, so on repositories where the build directory is gitignored, replace it with targeted deletions of the paths that hold secrets.

The 15-day lifetime

Snapshots are temporary and are deleted after 15 days. An alias that no workflow refreshes stops resolving after that window, and the next job requesting it boots from the base image and runs normally. Nothing fails, and the only visible symptom is a job that takes its cold-boot duration again. A workflow that saves on every merge to the default branch refreshes the alias continuously and never reaches the expiry, which is another argument for the save-on-main pattern above.

Boot behavior is the other timing fact worth knowing. Booting a snapshot runner can be slower than booting a default runner and takes 45 to 60 seconds, per the snapshot runners documentation. Snapshots pay for themselves when the state they carry costs more than a minute to rebuild, and the arithmetic below turns that into a threshold you can check against your own job durations.

What snapshot runners cost

Two line items sit on top of the runner's per-minute rate, both from the pricing page, checked on 2026-08-13:

  • Snapshot restore: $0.04 per job.
  • Snapshot storage: $0.025 per hour per snapshot.

The break-even is straightforward. Let r be the runner's per-minute rate and S the setup minutes the snapshot lets a job skip. Restoring costs one extra boot minute plus the $0.04 restore fee, so a snapshot pays off when S > 1 + 0.04 / r.

Runner labelPrice per minuteBreak-even setup time skipped
warp-ubuntu-latest-x64-2x$0.00411 minutes
warp-ubuntu-latest-x64-4x$0.0086 minutes
warp-ubuntu-latest-x64-8x$0.0163.5 minutes
warp-ubuntu-latest-x64-16x$0.0322.25 minutes
warp-ubuntu-latest-x64-32x$0.0641.6 minutes

The threshold falls as the runner gets larger, because the fixed $0.04 restore fee buys fewer minutes at a higher rate. On a 2 vCPU runner, a snapshot needs to remove more than 11 minutes of setup work to be worth its fee. On a 16 vCPU runner, 2.25 minutes is enough.

Here is a full month for a repository with a real workload. The assumptions are stated so you can substitute your own: 40 workflow runs per weekday across 22 weekdays, which is 880 runs per month; a job that takes 12 minutes on a clean boot, of which 7 minutes is environment setup; and one live snapshot alias refreshed on every merge, held for the whole month at 720 hours. With the snapshot, setup drops to zero and boot adds a minute, so the job takes 6 minutes.

The GitHub column prices the same 12-minute job on the 16-core Linux larger runner at $0.042 per minute, from the GitHub Actions billing reference, checked on 2026-08-13. GitHub-hosted runners have no snapshot equivalent, so that column carries no snapshot line items.

LineGitHub 16-core Linux larger runnerwarp-ubuntu-latest-x64-16x, no snapshotwarp-ubuntu-latest-x64-16x, snapshot
Rate per minute$0.042$0.032$0.032
Minutes per job12126
Runner minutes per month10,56010,5605,280
Runner cost per month$443.52$337.92$168.96
Snapshot restore feesnone$0$35.20
Snapshot storagenone$0$18.00
Total per month$443.52$337.92$222.16

The gap between the first and third columns is $221.36 per month for this workload. Per-size arithmetic for every label is on the pricing page. Two of the assumptions carry most of the weight. Setup minutes is the sensitive one: at 3 minutes of setup instead of 7, the snapshot column loses its advantage over the plain runner column, which is exactly what the break-even table predicts for a 16 vCPU runner. Storage is the other: $18.00 assumes a single alias alive continuously. Ten aliases held the same way is $180.00 per month, so per-branch snapshot aliases are worth counting before they are worth adopting.

Two structural notes belong with any of these numbers. The snapshot runners hub covers the rollout patterns, and the cache size limit guide covers the cases where a cache is the cheaper answer.

Do snapshot runners work on Windows, macOS, or BYOC runners?

No. Snapshot runners are supported only on WarpBuild Cloud Ubuntu runners. BYOC runners, Windows runners, and macOS runners are not supported, and snapshot labels on those runner types are silently ignored, so the job runs normally with no snapshot behavior and no error. The snapshot runners hub lists the Ubuntu labels that accept the feature.

How long does a snapshot last?

Snapshots are temporary and are deleted after 15 days. An alias that stops being refreshed therefore stops resolving after 15 days, and the next job that requests it boots from the base image instead of failing. The snapshot runners documentation is the reference for the lifetime and the save action inputs.

Are snapshot runners the same thing as a cache?

No. A cache action restores named paths into a clean machine, so apt packages, Docker images in the local daemon store, and files outside the cached paths are still missing. A snapshot restores the whole runner disk as it stood when the save action ran. The persistent caches guide walks through which of the two fits a given workflow.

What do snapshot runners cost?

Snapshot restore is billed at $0.04 per job and snapshot storage at $0.025 per hour per snapshot, on top of the per-minute rate of the Ubuntu runner you selected. The pricing page carries the per-minute rate for every runner label, and GitHub Actions runner defines the underlying terms.

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.