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 branch | Incremental snapshot on the branch | |
|---|---|---|
| Label on the producing job | warp-ubuntu-latest-x64-4x;snapshot.enabled=true | warp-ubuntu-latest-x64-4x;snapshot.key=<alias> |
| Label on the reading job | warp-ubuntu-latest-x64-4x;snapshot.key=<alias> | the same label, every run |
| Boots from | base image on the producer, published image on branches | the previous run under that alias |
| Refreshed by | pushes to the default branch | every push on the branch |
| Live aliases | one per repository | one per branch that runs the workflow |
| Storage per 30 day month | $18.00 | $18.00 for each alias kept alive |
| Failure mode | branch-only state is missing from the image | one bad run poisons the alias for that branch |
| Fits | shared dependency trees and toolchains | long 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: trueA 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: falseCompounding 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.
| Segment | Example | Why it is there |
|---|---|---|
| Project prefix | web-app | Keeps repositories in one organization apart |
| Cache shape | deps | One alias per kind of disk state, so a fixture alias never lands on a dependency job |
| Branch identity | pr-4821 | Unique per repository and safe inside a label, from github.event.pull_request.number |
| Shape version | v2 | Bump 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: falseThe 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"
doneThe 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.
Related Questions
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.