Vuncloud Blog
← Back to Cloud Lab

Why Is GitHub Actions iOS CI So Slow? CocoaPods, SPM & DerivedData Cache Guide (2026)

Cloud Lab · How to cache CocoaPods, SPM, and DerivedData on GitHub Actions · Full workflow examples ·~12 min read

GitHub Actions iOS CI cache optimization: CocoaPods SPM DerivedData Xcode slow builds
Bottom line first

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 repeats pod install
  • Swift Package Manager (SPM): dependency graph re-resolved; .build wiped 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).

30–60%
typical warm build time reduction
3
CocoaPods · SPM · DerivedData
1–5 min
common savings per layer (cache only)

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:

  1. Dependency re-resolution (CocoaPods / SPM)
  2. DerivedData cold starts (full recompile)
  3. 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).

Team reviewing GitHub Actions workflow for CocoaPods SPM DerivedData cache and iOS CI optimization

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)

CocoaPods cache · GitHub Actions
- 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)

SPM cache · iOS CI
- 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)

DerivedData cache · xcodebuild
- 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
restore-keys fallback

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)
Self-hosted fixed paths
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.

Self-hosted · fixed DerivedData + Pods
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:

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.

View Cloud Mac plans · CI/CD onboarding guide

Cloud Lab · iOS CI

GitHub Actions iOS CI slow? Check these three caches first

CocoaPods · SPM · DerivedData · workflow examples

See waterfall −1:40
Limited offer View plans