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 .git directory 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 surfaceSnapshot supportBehavior with a snapshot label
WarpBuild Cloud Ubuntu x64SupportedBoots from the snapshot or the base image per the label
WarpBuild Cloud Ubuntu ARM64SupportedBoots from the snapshot or the base image per the label
WarpBuild Cloud macOSUnsupportedLabel silently ignored, job runs normally
WarpBuild Cloud WindowsUnsupportedLabel silently ignored, job runs normally
BYOC on AWS, GCP, or AzureUnsupportedLabel 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.

LabelImagevCPURAMStoragePer 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

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 itemRateWhen it bills
Ubuntu runner time, x64$0.004 to $0.064 per minute by sizeEvery job minute
Ubuntu runner time, ARM64$0.003 to $0.048 per minute by sizeEvery job minute
Snapshot restore$0.04 per jobEach job that boots from a snapshot
Snapshot storage$0.025 per snapshot-hourWhile a snapshot exists
Cache storage$0.20 per GB-monthWhile cache entries exist
Cache write or restore$0.0001 per operationPer cache operation
Networking (Tailscale)FreeNot 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.

LabelRate per minuteMinutes a restore must remove to break even
warp-ubuntu-latest-x64-2x$0.00410.00
warp-ubuntu-latest-x64-4x$0.0085.00
warp-ubuntu-latest-x64-8x$0.0162.50
warp-ubuntu-latest-x64-16x$0.0321.25
warp-ubuntu-latest-x64-32x$0.0640.63
warp-ubuntu-latest-arm64-8x$0.0123.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.

LineWithout snapshotsWith one snapshot alias
Billed minutes per job20.012.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.

LineWithout snapshotsWith one snapshot alias
Billed minutes per job10.06.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=true turns the feature on. The runner always boots from the base image. Use snapshot-save to capture the machine at the point in the workflow you want later jobs to start from.
  • warp-ubuntu-latest-x64-4x;snapshot.key=my-alias turns 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: 30

The 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: 45

Every 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: 30

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