Can I Snapshot a Runner on a Feature Branch?

Yes. Publish a clean snapshot on the default branch and let feature branches boot from it, or give a branch its own alias and sweep it on close.

Yes. Snapshot behavior is requested by a label on the job, so any branch can boot from a snapshot or write one, and the two documented shapes are a clean snapshot published on the default branch that feature branches read with snapshot.key=<alias>, or an incremental alias that the branch refreshes on every push. Per branch aliases need a naming scheme that keeps them from overwriting each other and a cleanup job that removes them when the branch closes, because storage bills for every hour a snapshot exists.

Answer

Snapshot labels are appended to the runner label with a semicolon and are read per job. snapshot.enabled=true turns the feature on and always boots from the base image. snapshot.key=<alias> turns the feature on and boots from the existing snapshot for that alias when one exists, falling back to the base image when none does, per the snapshot runners documentation. Neither label carries any notion of a branch, so branch policy lives in the workflow expression that picks the label.

The documentation writes both shapes out as full examples. Here they are side by side.

Clean snapshot on the default branchIncremental snapshot on the branch
Label on the producing jobwarp-ubuntu-latest-x64-4x;snapshot.enabled=truewarp-ubuntu-latest-x64-4x;snapshot.key=<alias>
Label on the reading jobwarp-ubuntu-latest-x64-4x;snapshot.key=<alias>the same label, every run
Boots frombase image on the producer, published image on branchesthe previous run under that alias
Refreshed bypushes to the default branchevery push on the branch
Live aliasesone per repositoryone per branch that runs the workflow
Storage per 30 day month$18.00$18.00 for each alias kept alive
Failure modebranch-only state is missing from the imageone bad run poisons the alias for that branch
Fitsshared dependency trees and toolchainslong incremental builds and fixture data local to one branch

Both shapes cover the same part of the catalog. Snapshot runners are supported only on WarpBuild Cloud Ubuntu runners, per the cloud runners documentation. A snapshot label on a Windows, macOS, or BYOC runner is silently ignored, so the job runs with no snapshot behavior and no error.

Billing is identical across the two shapes: $0.04 per job that restores a snapshot and $0.025 per snapshot-hour of storage, from the pricing page, checked on 2026-08-13. Restore fees follow job count, so the branch decision is a storage decision, and the alias count is what moves it.

Detail

The default branch publishes, feature branches read

This is the first documented example. Pushes to main boot clean and republish the image, and pull request runs boot from it.

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-deps' }}
    steps:
      - 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
          fi

      - run: npm ci
      - run: npm run build

      - 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-deps"
          fail-on-error: true

A runner created from a snapshot sets WARPBUILD_SNAPSHOT_KEY to the alias it booted from, which is the signal the guard above reads. One alias exists for the whole repository, the image is rebuilt from a clean base on every merge, and feature branch work never enters it.

One alias per branch

The second documented example drops the producer and reader split. Every run boots from the alias and writes a new snapshot to it, so state compounds run over run on that branch.

jobs:
  build:
    runs-on: warp-ubuntu-latest-x64-4x;snapshot.key=web-app-deps-pr-4821
    steps:
      - uses: actions/checkout@v5
      - run: npm ci
      - run: cargo build --tests

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

      - uses: WarpBuilds/snapshot-save@v1
        with:
          alias: "web-app-deps-pr-4821"
          fail-on-error: false

Compounding is the point for a branch that rebuilds a large target directory or seeds fixture data, and it is also the risk. A run that leaves a broken toolchain on disk writes that disk to the alias, and the fix is to change the alias string, which sends the next job back to the base image.

An alias scheme that keeps branches from colliding

Three things collide when aliases are generated per branch. Two branches sharing an alias overwrite each other. Two workflows in one repository sharing an alias mix cache shapes, so a test fixture disk lands where a dependency disk was expected. And branch names carry / and ., which you would rather keep out of a label string. A pull request number sidesteps the last one, since it is already a safe token and unique within the repository.

SegmentExampleWhy it is there
Project prefixweb-appKeeps repositories in one organization apart
Cache shapedepsOne alias per kind of disk state, so a fixture alias never lands on a dependency job
Branch identitypr-4821Unique per repository and safe inside a label, from github.event.pull_request.number
Shape versionv2Bump it to force a base image boot after a toolchain change

That gives web-app-deps-pr-4821-v2. Building the label in a setup job keeps the expression readable and lets the same workflow serve both shapes, since runs-on accepts expressions including needs outputs per the GitHub workflow syntax reference.

jobs:
  setup:
    runs-on: warp-ubuntu-latest-x64-2x
    outputs:
      runner: ${{ steps.label.outputs.runner }}
      alias: ${{ steps.label.outputs.alias }}
    steps:
      - id: label
        run: |
          if [ "$GITHUB_REF" = "refs/heads/main" ]; then
            alias="web-app-deps-main-v2"
            echo "runner=warp-ubuntu-latest-x64-4x;snapshot.enabled=true" >> "$GITHUB_OUTPUT"
          else
            alias="web-app-deps-pr-${{ github.event.pull_request.number }}-v2"
            echo "runner=warp-ubuntu-latest-x64-4x;snapshot.key=$alias" >> "$GITHUB_OUTPUT"
          fi
          echo "alias=$alias" >> "$GITHUB_OUTPUT"

  build:
    needs: setup
    runs-on: ${{ needs.setup.outputs.runner }}
    steps:
      - uses: actions/checkout@v5
      - run: npm ci

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

      - uses: WarpBuilds/snapshot-save@v1
        with:
          alias: ${{ needs.setup.outputs.alias }}
          fail-on-error: false

The setup job runs on warp-ubuntu-latest-x64-2x at $0.004 per minute, from the pricing page, checked on 2026-08-13, and finishes inside a minute, so the label indirection costs well under a cent per run.

Deleting the alias when the branch closes

Doing nothing has a bounded cost. Snapshots are deleted after 15 days, so an alias that stops being refreshed when its branch merges caps out at $9.00, which is 15 days times 24 hours times $0.025. Reclaiming the storage sooner uses the snapshotter endpoints, GET /snapshots and DELETE /snapshots/{id}, both marked alpha in the API reference.

The delete call takes a snapshot id. WarpBuilds/snapshot-save publishes no outputs, and the documented Snapshot object carries id, status, provider, created_at, and external_id with no alias field, so a cleanup job matches on age rather than on the alias string.

name: snapshot-sweep
on:
  pull_request:
    types: [closed]
  schedule:
    - cron: "0 3 * * *"

jobs:
  sweep:
    runs-on: warp-ubuntu-latest-x64-2x
    steps:
      - name: Delete snapshots not refreshed in 3 days
        env:
          WARPBUILD_API_KEY: ${{ secrets.WARPBUILD_API_KEY }}
        run: |
          set -euo pipefail
          cutoff=$(date -u -d '3 days ago' +%Y-%m-%dT%H:%M:%SZ)
          curl -sf -H "Authorization: Bearer $WARPBUILD_API_KEY" \
            -H "Accept: application/json" \
            https://api.warpbuild.com/api/v1/snapshots \
            | jq -r --arg cutoff "$cutoff" \
              '.[] | select(.created_at < $cutoff) | .id' \
            | while read -r id; do
                curl -sf -X DELETE \
                  -H "Authorization: Bearer $WARPBUILD_API_KEY" \
                  "https://api.warpbuild.com/api/v1/snapshots/$id"
              done

The window is the safety rule: every alias you intend to keep has to be refreshed inside it. A shared alias republished on every merge clears a three day window easily. An alias written by a weekly workflow does not, so widen the window or skip that id explicitly. The pull_request: closed trigger is documented with the rest of the workflow events, and running the sweep on close as well as on a schedule keeps merged branches from waiting for the nightly pass.

The arithmetic decides how much any of this matters. One shared alias kept alive costs $18.00 per 30 day month. Twelve branch aliases open at once cost $216.00 over the same period, and forty merged branches whose aliases nobody deletes add $360.00 as they age out at $9.00 each. Restore fees stay the same across all three cases, since they track job count at $0.04 per job.

The snapshot runners hub carries the label reference and per-size rates, and the incremental builds guide covers the build systems that gain most from a warm disk.

Can two feature branches share one snapshot alias?

Yes, and the shared alias is the shape the documentation leads with. Every job that names the alias boots from the latest snapshot saved under it, so two branches saving to one alias overwrite each other's disk state run by run. Share an alias when the state is a dependency tree both branches want, and split the alias when one branch would poison the other. The snapshot runners hub lists the Ubuntu labels that accept snapshot keys.

What happens if a branch asks for an alias that was never created?

The runner boots from the base image and the job runs normally. snapshot.key=<alias> falls back to the base image when no snapshot exists for the alias, which is also what happens after the 15 day lifetime deletes one, so the first run on a new branch alias costs its usual cold setup time and nothing fails. The snapshot runners documentation is the reference for the fallback and the lifetime.

Does a branch alias have to be deleted when the branch merges?

No. Snapshots are deleted after 15 days, which caps an abandoned alias at $9.00 of storage at $0.025 per snapshot-hour, from the pricing page, checked on 2026-08-13. Deleting on merge reclaims that sooner, and it matters once branch aliases are counted in dozens rather than in ones. How do I invalidate a runner snapshot? covers the delete path and the alias rename that retires an image immediately.

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.