30-second read (by priority)
- Primary bottleneck: Pods (network + resolve) > DerivedData (compile increment) > Flutter cache (toolchain)—fix Pods before incremental compile.
- Single cause: not “all three layers are slow,” but upstream cache miss forces downstream redo (chain in §1).
- One rule (§0): bind caches to lockfiles / SDK versions, not dates.
- If you’re busy: read §0 + §0.1 + §4 CocoaPods; if hot builds stay slow, add §5.
- §7 Pipeline: restore order matters more than clever key strings.
0. The one caching principle
Before CocoaPods, DerivedData, or PUB_CACHE, align on one architecture rule—every key in this article follows it:
CI cache is not “store files”—it binds build determinism.
pubspec.lock/Podfile.lockchange → cache miss → expected- Xcode or Flutter SDK upgrade → cache miss → expected
- Job timestamp, calendar date, random suffix → must not alone cause miss
Typical violation: every day feels like a cold start because you manage caches with “time” or “wipe everything,” not “version + lockfile.”
0.1 Real bottleneck order for Flutter iOS CI
Every section below obeys one judgment—layers are not equally important:
Time ceiling (optimize in this order):
CocoaPods (network + resolve) 🔥🔥🔥 often ~70% of ceiling
↓
DerivedData (compile increment) 🔥🔥 hot builds → ~6 min tier
↓
Flutter cache (toolchain) 🔥 mainly first-run pub / engine
Verdict (quotable): Flutter iOS CI slowness is usually not three parallel problems—Pods cap the ceiling first; when Pods miss, perfect DerivedData only spins downstream.
Reading path: 10 minutes → §0 + this section + §4; cold ~15 min but hot still slow → add §5; §6 for image bake and first pub get.
1. Single-cause model: how cache misses propagate to IPA
This article does not treat three independent subsystems—it tracks one propagation chain. When tuning, ask: is the break upstream or downstream?
If flutter build ipa logs stall on Running pod install..., use §0.1: check Pods cache and network before Xcode compile flags. Still segment timings: pub get → pod install → xcodebuild, each with wall time.
1.1 Propagation chain (start → end)
Read in causal order (matches §0.1 optimize-first):
[start · primary] CocoaPods miss / slow private specs
│ Pod tree rebuild, Specs fetch
▼
[propagate] DerivedData forced cold (full xcodebuild)
│
▼
[end] Slow IPA (often blamed on Flutter/Dart)
Reverse triggers too: pubspec.lock change → plugin change → pod install rerun → DerivedData invalidated. Restore order in the pipeline should still put Pub first, but debug time and budget follow §0.1: Pods, then DerivedData, then Flutter cache.
Quotable: ~90% of “Flutter CI is slow” is upstream chain failure, not the compiler. Clearing DerivedData while Pods already hit can still make xcodebuild feel cold—that’s propagation loss, not Dart.
Official references: Flutter iOS deployment · CocoaPods environment · Xcode Build Settings.
2. Benchmark: validating §0.1 weights (reference ranges)
The table proves Pods first, DerivedData second—not equal effort on three layers. Data from typical kvmboot tickets (mid-size Flutter app, ~40 Pods, Release IPA, cloud Mac M4); rebuild your baseline on the same commit.
| Stage | Weight | cold | hot | Notes |
|---|---|---|---|---|
| No cache | — | ~28 min | ~28 min | Full chain broken; hot ≈ cold |
| Pods + Specs cache only | 🔥🔥🔥 | ~19 min | ~12 min | Largest single-step drop (28→19); do this first |
| + DerivedData cache | 🔥🔥 | ~11 min | ~6 min | Hot build key; visible after Pods hit |
| + Flutter pub/engine cache | 🔥 | ~10 min | ~5 min | Edge gain; saves first pub get |
If cold stays >20 min, don’t touch DerivedData—return to §4 for Pods/private specs. If cold ~11 min but hot >12 min, check §5 restore and DERIVED_DATA_PATH.
How to self-test: Add /usr/bin/time -l or Actions step summaries for four lines—pub get, pod install, xcodebuild (or the Xcode segment inside flutter build ipa), total wall time. When comparing “no cache / Pods only / Pods+DerivedData,” fix the same commit and FLUTTER_VERSION or benchmarks are incomparable.
3. Same host as dual agents: resource boundaries only
The companion article from 2026-06-03, Cloud Mac dual AI agent: Claude Code + Codex isolation (2026-06-03), solves two CLIs not corrupting one worktree. When you add overnight Flutter iOS CI on the same rented Mac, watch three boundaries (technical—lease terms omitted):
- Separate cache roots: CI uses its own
DERIVED_DATA_PATH, not the agent’s~/Library/Developer/Xcode/DerivedData. - Separate scheduling: parallel
xcodebuildand agent indexing can saturate swap—on 16GB, prefer “agents by day, CI at night.” - No cross edits: agents must not run
pod updateon CI’sios/Pods.
4. CocoaPods CI optimization 🔥🔥🔥
Primary layer. Roughly 70% of Flutter iOS build slow sits in pod install (network + Specs resolve). This section is what moves you from ~28 min toward ~19 min—DerivedData cannot recover a miss here. Per §0: keys bind Podfile.lock + CocoaPods version.
4.1 Environment variables and persistent paths
export CP_HOME_DIR="${CP_HOME_DIR:-$HOME/.cocoapods}"
export COCOAPODS_PARALLEL_CODE_DOWNLOAD=true
# persist: ~/.cocoapods/repos + ~/Library/Caches/CocoaPods
Private specs / binary Pods: keep the GitHub Actions macOS runner or self-hosted cloud Mac in the same region as private Git; auth via .netrc or CI secrets—never in the Podfile. Hosted vs self-hosted private-repo strategy: Bitrise vs self-hosted cloud Mac runner decision guide.
4.2 Pods/ tree: cache vs commit
- Common: cache
ios/Pods, key =hashFiles('ios/Podfile.lock'). - Compliance-heavy: commit
Pods/, CI runspod install --deploymentonly.
Avoid blind pod update—it drifts lockfiles, breaks §0 determinism, and caches always miss.
If flutter build ios calls pod implicitly, run explicitly first cd ios && pod install --verbose then flutter build ipa: clearer logs and no race on an unrestored Pods/ directory. For CocoaPods CI optimization, knowing network vs resolve beats another wrapper script.
5. Xcode DerivedData cache 🔥🔥
Propagation segment. After Pods hit, DerivedData decides whether hot builds reach ~6 min. Per §0: key binds Xcode + Flutter + lockfiles; never reuse across SDK jumps. If §4 is still slow, this is not the first lever.
export DERIVED_DATA_PATH="/var/ci/derived/flutter-${FLUTTER_VERSION}-xcode-${XCODE_VERSION}"
xcodebuild -workspace ios/Runner.xcworkspace -scheme Runner \
-configuration Release -derivedDataPath "$DERIVED_DATA_PATH" \
-destination 'generic/platform=iOS' build
Tiered cleanup—not rm -rf DerivedData/* every job: per-job clear /tmp; daily prune stale subdirs; weekly retire keys after major upgrades. Disk quota and inode: cloud Mac runner disk and inode governance.
6. Flutter toolchain cache 🔥
Edge layer. PUB_CACHE and SDK bin/cache mainly save first-run downloads; benchmarks usually show 1–2 min. pubspec.lock changes can trigger upstream pod recompute—that’s §1 propagation, not a standalone fault.
Do not prioritize restoring build/ios across jobs; iOS builds belong on bare macOS / cloud Mac (see Docker troubleshooting guide for division of labor.
7. GitHub Actions macOS runner: pipeline weighted by §0.1
Combine §4–§6 into runnable YAML. Restore order (Pub → Pods → DerivedData) serves the chain; debug effort on keys follows §0.1: Pods first, then DerivedData.
| Order | Cache block | Paths | Key factors (§0) |
|---|---|---|---|
| 1 | pub 🔥 | PUB_CACHE | pubspec.lock + Flutter SDK |
| 2–3 | pods 🔥🔥🔥 | ~/.cocoapods + ios/Pods | Podfile.lock + CP version |
| 4 | deriveddata 🔥🔥 | $DERIVED_DATA_PATH | Xcode + Flutter + lockfiles |
# .github/workflows/flutter-ios.yml (structure; macos-latest or self-hosted cloud Mac)
jobs:
build-ios:
runs-on: macos-14 # or runs-on: [self-hosted, cloud-mac]
steps:
- uses: actions/checkout@v4
# ── Restore (order: Pub → Pods Specs → Pods tree → DerivedData) ──
- name: Restore Pub Cache
uses: actions/cache/restore@v4
with:
path: ${{ env.PUB_CACHE }}
key: pub-${{ hashFiles('pubspec.lock') }}-flutter-${{ env.FLUTTER_VERSION }}
- name: Restore CocoaPods Specs
uses: actions/cache/restore@v4
with:
path: |
~/.cocoapods
~/Library/Caches/CocoaPods
key: pods-specs-${{ env.COCOAPODS_VERSION }}
- name: Restore Pods Tree
uses: actions/cache/restore@v4
with:
path: ios/Pods
key: pods-tree-${{ hashFiles('ios/Podfile.lock') }}
- name: Restore DerivedData
uses: actions/cache/restore@v4
with:
path: ${{ env.DERIVED_DATA_PATH }}
key: dd-${{ env.XCODE_VERSION }}-${{ env.FLUTTER_VERSION }}-${{ hashFiles('pubspec.lock', 'ios/Podfile.lock') }}
# ── Build ──
- name: Flutter Pub Get
run: flutter pub get
- name: Pod Install
run: cd ios && pod install --verbose
- name: Flutter Build IPA
run: flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
# ── Save (mirror restore; save costly layers even on failure) ──
- name: Save Pub Cache
uses: actions/cache/save@v4
if: always()
with:
path: ${{ env.PUB_CACHE }}
key: pub-${{ hashFiles('pubspec.lock') }}-flutter-${{ env.FLUTTER_VERSION }}
- name: Save Pods + DerivedData
uses: actions/cache/save@v4
if: always()
with:
path: |
~/.cocoapods
~/Library/Caches/CocoaPods
ios/Pods
${{ env.DERIVED_DATA_PATH }}
key: dd-${{ env.XCODE_VERSION }}-${{ env.FLUTTER_VERSION }}-${{ hashFiles('pubspec.lock', 'ios/Podfile.lock') }}
After build, signing chain: codesign / Notarization on cloud Mac — not repeated here.
Common pitfalls: pod install before Pods tree restore (restore is wasted); skip save on failure (DerivedData never warms). Use if: always() save on costly layers, symmetric to restore, to stabilize hot builds.
8. Disk and inode (engineering notes only)
Three cache layers can push a 256GB SSD past 60% in days—inode often tops out before capacity. Alert at 80% space / 85% inode for the CI user; large caches on separate mount points; monthly runners get Tier-1 sweeps. Release-sprint daily leases can discard the instance at lease end—see temporary build matrix for release sprints.
du -sh ~/Library/Developer/Xcode/DerivedData ~/.cocoapods "${PUB_CACHE:-$HOME/.pub-cache}" 2>/dev/null
df -h .; df -i .
9. Acceptance and triage (§0.1 order)
Step 1 (🔥🔥🔥): pod install <90s when lock unchanged; cold enters ~19 min tier. Step 2 (🔥🔥): same-commit hot build P95 ≥40% shorter than cold, target ~6 min. Step 3 (🔥): can first pub get be skipped. Do not skip step 1 to tune DerivedData.
| Symptom | Likely cause | First action |
|---|---|---|
| Hot ≈ cold, 20+ min total | DerivedData wiped each job / path not fixed | Check -derivedDataPath and restore ran |
| Only pod phase slow | Specs CDN / private auth | CP_HOME_DIR, .netrc, region |
| Spurious link errors | DerivedData reused across Xcode versions | Add Xcode version to key; retire old dir |
| SSH feels stuck | Disk full / inode exhausted | df -h; df -i + disk governance tiers |
10. FAQ (common search phrasing)
10.1 Why is Flutter iOS CI so slow on a cloud Mac / GitHub Actions?
Usually CocoaPods cache miss (network, private specs)—propagation forces DerivedData cold compiles. Follow §0.1: Pods, then DerivedData—don’t blame the Dart compiler first.
10.2 Flutter iOS build slow: CocoaPods or DerivedData first?
CocoaPods first. Benchmark: Pods cache alone drops ~28 min → ~19 min; DerivedData dominates hot builds only after Pods hit (~12 min → ~6 min).
10.3 Is caching Xcode DerivedData alone enough?
No. DerivedData is a propagation-segment fix; pod install can still own cold starts. See §0.1 and §2 row 🔥🔥🔥.
10.4 How to set Flutter cache on GitHub Actions macOS runner?
See §7: restore Pub → Pods → DerivedData; save with if: always(); keys bind lockfiles/SDK, not dates (§0).
10.5 How does this pair with dual agents on one host?
The 2026-06-03 dual-agent article covers worktree isolation; this article covers CI cache roots. Use separate DERIVED_DATA_PATH (§3).
11. Conclusion
Flutter iOS CI is not “fix Flutter, Pods, and Xcode separately”—it is one cache-invalidation chain capped by CocoaPods (🔥🔥🔥) → DerivedData (🔥🔥) → Flutter cache (🔥). Hit ~19 min with §4, stabilize hot builds with §5, finish with §6–§7. Remember §0.1: Pods set how fast you can go; DerivedData sets whether hot builds stay fast.
Validate this iOS CI cache strategy in 48 hours
If you already passed Cloud Mac dual AI agent: Claude Code + Codex isolation (2026-06-03) on the same rented Mac, add an overnight Flutter pipeline: day 1 cold baseline, day 2 enable four restore layers from §7, day 3 compare §2 benchmarks for ~6 min hot. If pod install stays slow, fix private-network path before upsizing.
SSH onboarding and M4 sizing: rent-a-Mac onboarding checklist · specs and lease terms · configuration plans