Can I Keep DerivedData Between Runs?
Yes. Cache the DerivedData directory keyed on your lockfile and Xcode version, or keep prepared machine state. What restores safely and what breaks.
Yes, DerivedData can survive from one GitHub Actions run to the next, by one of two mechanisms: storing the directory as a cache entry keyed on your lockfile and toolchain, or keeping the whole prepared machine state so the disk itself comes back. The two behave differently on invalidation, and only part of a DerivedData tree is safe to restore at all.
Answer
A GitHub Actions runner is ephemeral, so xcodebuild starts against an empty DerivedData tree on every run and recompiles everything. Two mechanisms put state back.
Cache the directory. Point xcodebuild at an explicit -derivedDataPath inside the workspace, then restore and save the useful subdirectories with a cache action. On WarpBuild runners this is WarpBuilds/cache@v1, a drop-in replacement for actions/cache@v4 that takes the same path, key, and restore-keys inputs and produces the same cache-hit output. The WarpBuild caching documentation covers the action and its subactions, and the cache is available on all WarpBuild runners.
Keep the prepared machine. A snapshot runner boots a job from a saved image of an earlier run's virtual machine, so everything the earlier run left on disk is present at step one, including files no cache step was ever configured to track. Snapshot runners are supported only on WarpBuild Cloud Ubuntu runners; on macOS runners the snapshot labels are silently ignored and the job runs normally, per the snapshot runners documentation. For an Xcode job on macOS, the cache path is the mechanism that applies today, and the machine-state path applies to Linux jobs in the same pipeline.
Labels, sizes, and rates for the macOS fleet are on the macOS runners page.
Detail
The two mechanisms side by side
| Dimension | Cached DerivedData directory | Prepared machine state |
|---|---|---|
| What comes back | Only the paths listed in path | The whole runner disk as saved |
| Addressed by | An immutable key string, usually a lockfile hash | A mutable alias in the runs-on label |
| Miss behavior | restore-keys prefix match, then empty | Boots from the base image, job proceeds |
| Invalidation | New key writes a new entry; old entries stay | Next save overwrites the alias |
| Expiry | 7 days after last use | 15 days |
| Platform | All WarpBuild runners, including macOS | Cloud Ubuntu runners only |
| Cost lines | $0.20 per GB-month, $0.0001 per operation | $0.04 per restore, $0.025 per snapshot-hour |
Rates come from the pricing page, checked on 2026-08-13, and both cache lines are free on BYOC. The invalidation row is the one that decides most designs. A cache key is immutable once written, so a bad restore is repaired by changing the key and the next run starts clean. A snapshot alias compounds, so a bad run poisons the alias until you rename it.
One more constraint applies to the cache path. 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, as the caching documentation records. Keep the runner label stable across the workflows that write and read a DerivedData entry.
What is safe to restore
DerivedData is several trees with different lifetimes under one directory, and restoring all of them is what produces stale-build failures.
Build/Productsholds the built.app,.xctest, and framework bundles. Safe, and the piece that makes abuild-for-testingthentest-without-buildingsplit worth doing.Build/Intermediates.noindexholds object files, generated headers, and.swiftmoduleoutputs. Safe when the checkout path is identical between save and restore, because these files record absolute paths.ModuleCache.noindexholds precompiled clang modules that are validated against the compiler and the headers they were built from. Restore it after an Xcode change and the build fails on module validation rather than falling back to a rebuild. Leave it out.Index.noindexis the editor index store. Nothing in a command line build reads it, and it is often the largest tree in DerivedData.Logsis build and test logs. No value to a later run.
The absolute path rule is the one that catches teams first. DerivedData records the path it was built at, so -derivedDataPath has to resolve to the same location on the run that saves and the run that restores. Put it under ${{ github.workspace }} and the runner keeps it stable.
Put the Xcode version in the key as well. The macOS 26 image ships the Xcode 27.0 SDKs and simulator runtimes on top of the upstream GitHub macOS 26 image, while GitHub's upstream macOS 27 runner image is in beta; a dedicated macOS 27 image follows once that image is released, per the cloud runners documentation. A pipeline that moves between macOS images changes compilers, so an entry from the old compiler should miss rather than restore.
The workflow shape
name: ios-build
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: warp-macos-latest-arm64-6x
env:
DERIVED_DATA: ${{ github.workspace }}/DerivedData
steps:
- uses: actions/checkout@v5
- name: Record toolchain
id: toolchain
run: |
echo "xcode=$(xcodebuild -version | head -1 | tr ' ' '-')" >> "$GITHUB_OUTPUT"
- name: Restore DerivedData
uses: WarpBuilds/cache@v1
with:
path: |
DerivedData/Build/Products
DerivedData/Build/Intermediates.noindex
key: dd-${{ steps.toolchain.outputs.xcode }}-${{ hashFiles('**/Package.resolved') }}-${{ github.sha }}
restore-keys: |
dd-${{ steps.toolchain.outputs.xcode }}-${{ hashFiles('**/Package.resolved') }}-
dd-${{ steps.toolchain.outputs.xcode }}-
- name: Build for testing
run: |
xcodebuild build-for-testing \
-scheme App \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-derivedDataPath "$DERIVED_DATA"The key has three segments and the restore-keys ladder drops one segment at a time. The primary key ends in the commit SHA, so every run writes a fresh entry and the entry a pull request restores is the closest previous build with the same toolchain and the same resolved packages. The second rung keeps a build warm across commits. The third keeps it warm across a dependency bump, which restores intermediates the new package graph will partly discard, still cheaper than a full recompile.
Guard nothing on cache-hit here. That output is true only on an exact primary key match, and with a SHA in the key it is almost always false even when the second rung restored several gigabytes. The build step reads the restored tree either way.
What it costs to keep
A worked month, with the assumptions stated so you can substitute your own: 880 workflow runs, a DerivedData entry of 4 GB kept warm for the whole month, one restore and one save per run, and warp-macos-latest-arm64-6x at $0.08 per minute from the pricing page, checked on 2026-08-13.
| Line | Amount |
|---|---|
| Cache storage, 4 GB for one month | $0.80 |
| Cache operations, 1,760 at $0.0001 | $0.18 |
| Total cache charge | $0.98 |
| Runner minutes returned by 4 minutes saved per run | 3,520 |
| Runner cost of those minutes at $0.08 | $281.60 |
Treat the four minutes as an input to substitute: run the build once cold and once warm on your own repository, then put your own number in the table. The shape of the trade holds at almost any value, because a cache entry that size costs under a dollar a month while macOS minutes are the expensive line. The Xcode build times guide covers the other levers on the same job.
Related Questions
Does caching DerivedData work on WarpBuild macOS runners?
Yes. The WarpBuild cache is available on all WarpBuild runners, and WarpBuilds/cache@v1 is a drop-in replacement for actions/cache@v4 with the same path, key, and restore-keys inputs. Cache entries carry a version derived from the runner OS and the cached paths, so an entry written on a macOS runner never restores on a Linux runner. The caching documentation has the full action reference, and the DerivedData cache guide walks the setup end to end.
Can snapshot runners keep DerivedData for an Xcode job?
No. Snapshot runners are supported only on WarpBuild Cloud Ubuntu runners. On macOS runners the snapshot labels are silently ignored and the job runs normally with no snapshot behavior and no error, per the snapshot runners documentation. Use the cache path for Xcode jobs and read snapshot runners for the Linux jobs in the same pipeline.
Which DerivedData subdirectories should stay out of the cache?
ModuleCache.noindex, Index.noindex, and Logs. The module cache is validated against the compiler and the headers it was built from, which turns a toolchain change into a build failure instead of a rebuild. The index store and the logs add gigabytes that no command line build reads. Keep Build/Products and Build/Intermediates.noindex, and see the macOS runners page for the image each label carries.
What does keeping DerivedData between runs cost?
Cache storage is $0.20 per GB-month and each cache write or restore is $0.0001 on hosted runners, both free on BYOC, from the pricing page, checked on 2026-08-13.
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.