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 platformExample labelSnapshot runners
Linux x64warp-ubuntu-latest-x64-4xSupported
Linux ARM64warp-ubuntu-latest-arm64-4xSupported
macOSwarp-macos-latest-arm64-6xUnsupported, label ignored
Windowswarp-windows-latest-x64-4xUnsupported, label ignored
BYOC runners on any cloudcustomer-definedUnsupported, 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 -derivedDataPath to a location inside ${{ github.workspace }} so the absolute path is identical on the run that saves and the run that restores. Restore Build/Products and Build/Intermediates.noindex, and leave out ModuleCache.noindex, Index.noindex, and Logs.
  • The SwiftPM checkout directory, pinned with xcodebuild -clonedSourcePackagesDirPath rather 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 Pods directory in the checkout plus ~/Library/Caches/CocoaPods and ~/.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: false

Three 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

MechanismLine itemRate
Snapshot runner, Linux onlySnapshot restore$0.04 per job
Snapshot runner, Linux onlySnapshot storage$0.025 per snapshot-hour
Cache, all runners including macOSCache storage$0.20 per GB-month
Cache, all runners including macOSCache write or restore$0.0001 per operation
macOS runner, 6 vCPUwarp-macos-latest-arm64-6x$0.08 per minute
macOS runner, 12 vCPUwarp-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.

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.