Do Snapshot Runners Work on macOS?
No. Snapshot runners are documented for Linux x64 and Linux ARM64 only, so a macOS job keeps warm state through cached DerivedData and package directories.
No. Snapshot runners are documented for Linux x64 and Linux ARM64 runners, and macOS runners are listed as an unsupported platform, so an Xcode job cannot boot from a saved machine image. A macOS job keeps warm state the other way, by caching the directories that hold compiled output and resolved packages.
Answer
A snapshot runner boots a GitHub Actions job from a saved image of an earlier run's virtual machine, so packages, dependency trees, and build output an earlier run left on disk are present at step one. The snapshot runners documentation states the platform boundary in its prerequisites: snapshot runners are supported only on WarpBuild Cloud Ubuntu runners, and BYOC runners, Windows runners, and macOS runners are not supported. The feature entry in the runner catalog records the same two platforms, linux-x64 and linux-arm64.
The failure mode matters more than the boundary. Snapshot labels on an unsupported runner type are silently ignored, so a macOS job with snapshot.key=ios-main in its runs-on value starts, runs, and passes with no snapshot behavior and no message saying why. A workflow that was written expecting warm state gets cold-boot durations and no error to explain them.
Labels, sizes, and rates for the macOS fleet are on the macOS runners page, and the snapshot runners hub covers the Ubuntu labels that accept the feature.
Detail
Platform support row from the feature catalog
| Runner platform | Example label | Snapshot runners |
|---|---|---|
| Linux x64 | warp-ubuntu-latest-x64-4x | Supported |
| Linux ARM64 | warp-ubuntu-latest-arm64-4x | Supported |
| macOS | warp-macos-latest-arm64-6x | Unsupported, label ignored |
| Windows | warp-windows-latest-x64-4x | Unsupported, label ignored |
| BYOC runners on any cloud | customer-defined | Unsupported, label ignored |
The two supported rows come from the snapshot feature entry in the runner catalog, and the unsupported rows come from the prerequisites section of the snapshot runners documentation. Labels for every platform are listed in the cloud runners documentation.
What a macOS job uses instead
The mechanism that carries state onto a macOS runner is the cache. WarpBuilds/cache@v1 is a drop-in replacement for actions/cache@v4 that takes the same path, key, and restore-keys inputs and returns the same cache-hit output, and the caching documentation records that the cache is available on all WarpBuild runners. Four paths carry most of the warm state in an Xcode job:
- The DerivedData tree, redirected with
xcodebuild -derivedDataPathto a location inside${{ github.workspace }}so the absolute path is identical on the run that saves and the run that restores. RestoreBuild/ProductsandBuild/Intermediates.noindex, and leave outModuleCache.noindex,Index.noindex, andLogs. - The SwiftPM checkout directory, pinned with
xcodebuild -clonedSourcePackagesDirPathrather than left at its default under~/Library/Developer/Xcode/DerivedData/*/SourcePackages. - The shared SwiftPM directory at
~/Library/Caches/org.swift.swiftpm, which holds repository clones and parsed manifests reused across packages on the machine. - For a CocoaPods project, the
Podsdirectory in the checkout plus~/Library/Caches/CocoaPodsand~/.cocoapods/repos.
Two constraints apply to any of those entries. A cache entry carries a version derived from the runner operating system and the cached paths, so an entry written on warp-macos-14-arm64-6x does not restore on warp-ubuntu-latest-x64-4x. And the Xcode version belongs in the key, because module output does not survive a compiler change and a restore that the compiler later rejects costs more than a miss. The Xcode DerivedData cache guide works through the key design and the save-from-main pattern in full.
An iOS pipeline that also has Linux jobs
Most iOS repositories run more than the app build: an API, a set of integration tests, container images. Those jobs sit on Linux runners, where snapshot runners do apply, so one workflow ends up running both mechanisms side by side. Snapshot labels live in the runs-on value of each individual job, so the split is per job and needs no separate workflow file.
name: mobile
on:
push:
branches: [main]
pull_request:
jobs:
ios:
runs-on: warp-macos-latest-arm64-6x
steps:
- uses: actions/checkout@v5
- name: Record toolchain version
id: toolchain
run: echo "xcode=$(xcodebuild -version | tr '\n' '-')" >> "$GITHUB_OUTPUT"
- name: Restore DerivedData and SwiftPM checkouts
uses: WarpBuilds/cache/restore@v1
with:
path: |
${{ github.workspace }}/.derived-data/Build
${{ github.workspace }}/.spm-checkouts
~/Library/Caches/org.swift.swiftpm
key: ios-${{ steps.toolchain.outputs.xcode }}-${{ hashFiles('**/Package.resolved') }}-${{ github.sha }}
restore-keys: |
ios-${{ steps.toolchain.outputs.xcode }}-${{ hashFiles('**/Package.resolved') }}-
- name: Build and test
run: |
xcodebuild test \
-scheme App \
-destination "platform=iOS Simulator,name=iPhone 16" \
-derivedDataPath "${{ github.workspace }}/.derived-data" \
-clonedSourcePackagesDirPath "${{ github.workspace }}/.spm-checkouts"
- name: Save from main only
if: github.ref == 'refs/heads/main'
uses: WarpBuilds/cache/save@v1
with:
path: |
${{ github.workspace }}/.derived-data/Build
${{ github.workspace }}/.spm-checkouts
~/Library/Caches/org.swift.swiftpm
key: ios-${{ steps.toolchain.outputs.xcode }}-${{ hashFiles('**/Package.resolved') }}-${{ github.sha }}
api:
runs-on: >-
${{ github.ref == 'refs/heads/main'
&& 'warp-ubuntu-latest-x64-4x;snapshot.enabled=true'
|| 'warp-ubuntu-latest-x64-4x;snapshot.key=api-main' }}
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 protobuf-compiler
fi
- name: Test
run: ./scripts/test.sh
- name: Cleanup credentials
if: github.ref == 'refs/heads/main'
run: |
rm -rf $HOME/.ssh $HOME/.aws
git clean -ffdx
- uses: WarpBuilds/snapshot-save@v1
if: github.ref == 'refs/heads/main'
with:
alias: "api-main"
fail-on-error: falseThree details in that file are worth calling out. WARPBUILD_SNAPSHOT_KEY is set only on a machine that booted from a snapshot, so the guard around apt-get is what turns a warm boot into saved minutes. The credential cleanup step runs before every save, because a snapshot carries the whole disk and any token left in a home directory travels with it. And the macOS job has no equivalent guard, because a cache restore reports its result through cache-hit rather than through an environment variable.
The expiry windows differ too. Snapshots are deleted after 15 days, so an alias that no workflow refreshes stops resolving and the next job boots from the base image and runs normally. Cache entries follow the repository eviction rules in GitHub's caching documentation, which is why the save step above is guarded on the default branch: pull request jobs that each write a DerivedData entry evict the entry everything else restores from.
What each mechanism costs
| Mechanism | Line item | Rate |
|---|---|---|
| Snapshot runner, Linux only | Snapshot restore | $0.04 per job |
| Snapshot runner, Linux only | Snapshot storage | $0.025 per snapshot-hour |
| Cache, all runners including macOS | Cache storage | $0.20 per GB-month |
| Cache, all runners including macOS | Cache write or restore | $0.0001 per operation |
| macOS runner, 6 vCPU | warp-macos-latest-arm64-6x | $0.08 per minute |
| macOS runner, 12 vCPU | warp-macos-latest-arm64-12x | $0.16 per minute |
Rates come from the pricing page, checked on 2026-08-13, and both cache lines are free on BYOC. On the macOS side the arithmetic is easy to run for a repository with 880 monthly workflow runs, 40 per weekday across 22 weekdays: a 4 GB DerivedData entry held for a month is $0.80 in storage, and 880 runs that each restore and save it is $0.176 in operations, against $0.08 per minute of runner time. One minute of compilation saved per run on the 6 vCPU label is $70.40 per month, so the cache pays for itself long before the storage line becomes visible.
Related Questions
What happens if I put a snapshot label on a macOS runner?
Nothing visible. The snapshot.enabled=true and snapshot.key labels are silently ignored on unsupported runner types, so the job runs normally with no snapshot behavior, no warning, and no error in the log. The only way to tell a working snapshot job from an ignored label is to check whether WARPBUILD_SNAPSHOT_KEY is set, as the snapshot runners documentation describes.
What keeps an Xcode job warm on macOS instead?
A cache entry. Point xcodebuild at an explicit -derivedDataPath and -clonedSourcePackagesDirPath inside the workspace, then restore and save those paths plus ~/Library/Caches/org.swift.swiftpm with WarpBuilds/cache@v1, which is a drop-in replacement for actions/cache@v4 and is available on all WarpBuild runners. The Xcode DerivedData cache guide covers which subdirectories are safe to restore.
Can one pipeline use snapshot runners for its Linux jobs and caching for its macOS jobs?
Yes. Snapshot labels live in the runs-on value of each job, so a Linux job can carry snapshot.key while the macOS job in the same workflow uses a plain macOS label and a cache step. The two mechanisms share nothing and are billed separately, as the rate table above shows. The snapshot runners hub lists the Ubuntu labels that accept the feature, and the macOS runners page lists the five macOS labels and their sizes.
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.