When GitHub Actions iOS CI feels slow, the usual culprit isn’t raw CPU—it’s missing cache or wrong cache keys, in three places:
- CocoaPods: no
Pods/cache → every run repeatspod install - Swift Package Manager (SPM): dependency graph re-resolved;
.buildwiped each job - Xcode DerivedData: cold builds trigger full Swift compile and module rebuild
Wire up CocoaPods cache on GitHub Actions, SPM cache, and DerivedData cache correctly and warm build time often drops 30%–60% (Shadow comparison in the waterfall breakdown).
1. Why GitHub Actions iOS CI drags
Most teams ask first: “Is macos-latest underpowered?” “Should we jump to M4 or bigger iron?”
In real Xcode CI slowdown cases, the bottleneck is rarely CPU. It’s three hidden costs:
- Dependency re-resolution (CocoaPods / SPM)
- DerivedData cold starts (full recompile)
- actions/cache misses—every run feels like a first build
GitHub Actions amplifies this: runners are ephemeral (clean disk each job), cache never “just sticks,” and multi-branch / multi-job setups pollute keys. If you’re chasing why the same commit builds fast one run and slow the next, split cold vs warm first, then tune cache using this guide.
2. Where iOS CI time actually goes
2.1 CocoaPods (Pods cache)
Typical cold cost: pod install about 3–8 minutes.
Root cause: every CI run re-runs pod install—resolve, download, integrate from scratch.
Fix: cache Pods/ with actions/cache; key = Podfile.lock hash + github.ref + arm64. In CI use pod install --deployment.
2.2 Swift Package Manager (SPM)
Typical cost: resolving the package graph 1–5 minutes.
Root cause: re-resolve every run; .build wiped; Package.resolved not in the cache key.
Fix: cache ~/Library/Caches/org.swift.swiftpm and .build; key includes Package.resolved hash (see §3.2).
2.3 Xcode DerivedData
Typical cold increment: full or near-full compile 5–15 minutes.
Root cause: DerivedData not reused → full Swift compile, asset reprocessing, module rebuild.
Fix: xcodebuild -derivedDataPath DerivedData and cache that path (see §3.3).
3. A working GitHub Actions iOS CI cache setup
For runs-on: macos-latest. Drop all three blocks into the same job, before pod install / xcodebuild (actions/cache@v4 saves automatically).
3.1 CocoaPods cache (required)
- name: Restore Pods cache
uses: actions/cache@v4
with:
path: Pods
key: pods-arm64-${{ github.ref }}-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
pods-arm64-${{ github.ref }}-
- name: Pod install
run: pod install --deployment
3.2 SPM cache (required)
- name: Restore SPM cache
uses: actions/cache@v4
with:
path: |
~/Library/Caches/org.swift.swiftpm
.build
key: spm-arm64-${{ github.ref }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
spm-arm64-${{ github.ref }}-
3.3 DerivedData cache (high impact)
- name: Restore DerivedData cache
uses: actions/cache@v4
with:
path: DerivedData
key: dd-arm64-${{ github.ref }}-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
dd-arm64-${{ github.ref }}-
- name: Build iOS
run: xcodebuild -scheme MyApp -derivedDataPath DerivedData ...
Wait for Cache hit occurred in logs before comparing step times. On a miss, fix key and path first—don’t blame the Xcode version yet.
4. Cache key design (don’t skip this)
Many iOS CI optimization efforts fail on keys, not runners.
| ✔ Must include | ❌ Common mistakes |
|---|---|
Architecture (arm64) |
No arm64 / x86 split—switch runner and everything misses |
Branch (github.ref) |
Lockfile only—DerivedData bleeds across branches |
| Lockfile hash | restore-keys too broad (e.g. single prefix dd-) → wrong hit |
When the lockfile changes, a prefix like dd-arm64-${{ github.ref }}- can partially reuse cache. Multi-scheme? Add ${{ matrix.scheme }} to the key. Concurrent jobs on one runner must not share one DerivedData root—isolate with ${{ github.run_id }} subdirs.
5. Self-hosted / Cloud Mac (advanced)
On a Mac mini or Cloud Mac self-hosted runner, you’re not limited to actions/cache—disk persists and warm builds stay steadier:
- Fixed DerivedData path
- Fixed Pods directory
- On-disk warm cache (saves 30–90 s upload/download per job)
export DERIVED_DATA=/Volumes/Data/DerivedData/App-${{ github.run_id }}
export PODS_ROOT=/Volumes/Data/Pods/${{ github.ref_name }}
Full job example and 1 TB data disk notes: workflow below and CI/CD onboarding guide.
env:
DERIVED_DATA: /Volumes/Data/DerivedData/App-${{ github.run_id }}
PODS_ROOT: /Volumes/Data/Pods/${{ github.ref_name }}
jobs:
build:
runs-on: [self-hosted, macos-m4-ios]
steps:
- uses: actions/checkout@v4
- name: Prepare dirs
run: mkdir -p "$DERIVED_DATA" "$PODS_ROOT"
- name: Pod install
run: pod install --deployment
- name: xcodebuild
run: xcodebuild -scheme MyApp -derivedDataPath "$DERIVED_DATA" build
6. Cache miss ≠ slow runner (common misread)
Teams often blame slow GitHub Actions iOS CI on the runner or Xcode version. More often:
| What you see | More likely cause |
|---|---|
| Build suddenly slower | Cache miss |
| P95 wobble | Cold builds mixed into warm stats |
| Random slowdowns | DerivedData contention across concurrent jobs |
| Dependencies rebuilt from scratch | Wrong cache key |
Pair with the build variance guide: split cold/warm → tune cache → then evaluate hardware (Why iOS CI/CD runs on Mac mini M4).
7. Measured gains
Cache-only fixes, typical savings:
| Layer | Extra time on cold miss | After cache wired |
|---|---|---|
| CocoaPods | 3–8 min | warm often <30 s–3 min |
| SPM | 1–5 min | resolve much shorter on hit |
| DerivedData | 5–15 min | warm mostly incremental compile |
In our 14-day Shadow run, cache optimization alone cut warm wall clock by about −1:40. Add queue elimination and self-hosted fixed disk and warm P95 went 14:12 → 6:05 (−57%)—see the optimization overview.
8. FAQ
Why is GitHub Actions iOS CI so slow?
Usually not CPU—it’s CocoaPods / SPM / DerivedData uncached or mis-keyed. Ephemeral runners amplify every miss.
How do I cache CocoaPods on GitHub Actions?
Cache Pods/; key: pods-arm64-${{ github.ref }}-${{ hashFiles('**/Podfile.lock') }}. See §3.1.
How do I cache SPM in iOS CI?
Cache the swiftpm global dir and .build; bind key to Package.resolved. See §3.2.
What path should DerivedData cache use?
actions/cache path must match xcodebuild -derivedDataPath; you don’t need the system default.
Cache too large?
GitHub caps each cache entry around 10 GB. Split DerivedData keys by scheme, or use self-hosted fixed disk and skip upload.
macos-latest vs self-hosted?
Hosted: only actions/cache, may clear between jobs. Self-hosted: fixed /Volumes/Data, steadier warm builds. Compare in the optimization overview.
9. Wrap-up and related guides
The highest-ROI step in iOS CI optimization is often wiring CocoaPods, SPM, and DerivedData caches—before swapping chips. You can change the workflow today and run a test PR.
Same series:
- iOS CI Builds Feel Slow? Why Xcode Builds Drag on GitHub Actions — cold/warm, P95 stats
- GitHub Actions macOS Runner Optimization — P95 −57%, Shadow waterfall
- Buy or rent · 500 builds/month: buy or rent?
- Why iOS CI/CD Runs on Mac mini M4 in 2026 · CI/CD onboarding guide
Want fixed DerivedData and Pods paths?
Vuncloud Cloud Mac M4 Pro ships with a 1 TB data disk—built for self-hosted iOS CI: less shuffling via actions/cache, steadier warm builds.