Offer

Why Is xcodebuild 2–3× Slower in CI Than on Your Mac?

CI Flutter iOS CI
2026-06-08 ~12 min read

If your Flutter iOS pipeline feels unpredictably slow in GitHub Actions, this article shows where the time really goes and how to recover it with reproducible cache and runner architecture.

Flutter iOS CI timeline showing local vs CI xcodebuild gap
A typical Flutter iOS pipeline can look healthy in local release builds but still run 2-3x slower in GitHub Actions when the runner is stateless.

Key takeaways before you tune

  1. xcodebuild itself is not the only culprit; cache lifecycle and machine lifecycle dominate total time.
  2. GitHub-hosted jobs behave like cold boots, while a self-hosted runner behaves like a warm workstation.
  3. DerivedData and CocoaPods cache persistence usually removes the biggest chunk of wasted CI minutes.
  4. Disk pressure and signing setup noise can hide as random slowdowns unless you time each stage.
  5. For Flutter teams, rent a mac first for a 48-hour profile, then move to a monthly self-hosted runner.

Local machine vs GitHub Actions: where the extra minutes come from

When you benchmark locally, the setup usually looks like this:

  • Local: persistent SSD + warmed toolchain state; CI: ephemeral VM and cold dependency graph.
  • Local: same user keychain and path conventions; CI: one-time signing bootstrap per run.
  • Local: repeatable branch builds on same host; CI: many branches forcing cache misses.

A typical CI pipeline does the opposite:

  • Fresh checkout on a clean workspace every run.
  • flutter build ipa or xcodebuild archive in Release, not Debug.
  • Hosted runners often cannot keep caches, so each build starts cold.

When teams say “CI is 2-3x slower”, they often compare only the final `xcodebuild archive` number. The operational truth is broader: checkout depth, pod install, simulator runtime prep, keychain unlock, and DerivedData invalidation all stack up before the compiler does real work.

Five reasons xcodebuild becomes 2-3x slower in CI

These five causes appear repeatedly in Flutter iOS pipelines, including teams on GitHub Actions and teams migrating to self-hosted runners.

  1. 1) Stateless runners reset every dependency layer A GitHub-hosted VM is rebuilt per job, so SPM checkout, pod install side effects, module cache, and Swift index state are never truly hot. Even with actions/cache, restore misses and key drift keep performance unstable.
  2. 2) DerivedData path changes invalidate build graph If DerivedData uses random temporary paths, incremental compile and index reuse collapses. xcodebuild then recompiles portions that your local machine would skip.
  3. 3) CocoaPods + Flutter plugin resolution does cold work Flutter plugin updates can trigger pod lock churn. Without strict lockfile discipline and deployment mode, `pod install` can spend minutes resolving and downloading.
  4. 4) Signing and keychain steps are serialized overhead Importing certificates, creating keychains, and unlocking signing identities in CI adds latency before the archive step. On local environments this context is already resident.
  5. 5) Disk and inode pressure silently throttles builds Many teams focus on CPU only. In reality, limited free SSD, cache fragmentation, and inode saturation can make indexing and file writes crawl, especially on shared hosted images.

Stage timing log (sample from a Flutter iOS workflow)

Use this table format to profile your own runs. Time each stage separately before you switch providers or rewrite workflows.

Stage Local Mac GitHub Actions What usually happened
flutter pub get0m25s1m10spub cache not fully warm
pod install1m30s4m45scold Specs + plugin pod resolution
xcodebuild compile/archive6m40s12m20sDerivedData mismatch and no module reuse
codesign/export0m55s2m00skeychain bootstrap each run
total9m30s20m15sabout 2.1x slower

Before you blame xcodebuild

Before you tune xcodebuild, look at pod install. CocoaPods is slow in CI for three recurring reasons:

  • Podfile.lock not committed — dependency resolution becomes unpredictable.
  • No cache for ios/Pods — hosted runners delete artifacts after the job.
  • Extra pod repo update — usually unnecessary when the lockfile pins versions.

Use pod install --deployment in CI and keep Pods on the same SSD path. Self-hosted Mac mini runners avoid uploading cache archives to GitHub and downloading them again.

DerivedData, Pods, and cache: the practical architecture

For Flutter iOS CI, the biggest gains usually come from pinning DerivedData to a stable absolute path and keeping pod caches predictable. If you run a self-hosted runner, this persistence turns CI from a disposable machine into a long-lived build appliance.

If you stay on GitHub Actions hosted runners, still enforce deterministic paths and lockfiles so cache restore can do real work. If you can migrate, a self-hosted runner on rented Apple Silicon usually makes warm-path behavior close to local builds.

  • Use one derivedDataPath per app target, not random temp folders.
  • Run `pod install --deployment` to prevent lockfile drift.
  • Watch free disk and inode consumption as first-class CI metrics.

Fix pattern: stable paths + deterministic dependency restore

The snippet below is intentionally conservative. It avoids fragile YAML tricks and focuses on path stability and predictable dependency behavior.

  1. Add concurrency to the workflow — cancel stale builds on the same branch.
  2. Run tests on Linux — do not spend Mac minutes on flutter test before xcodebuild.
  3. Persist Pods + DerivedData — fixed paths on self-hosted hosts; granular cache keys on hosted runners.
  4. Build only the shipping flavor — skip scanning six flavors when you upload one IPA.
  5. Pin Xcode and Flutter versions — avoid brew upgrade or flutter upgrade inside CI.
# .github/workflows/ios-ci.yml
jobs:
  ios:
    runs-on: [self-hosted, macOS, ARM64, flutter-ios]
    steps:
      - uses: actions/checkout@v4
      - name: Restore caches
        run: |
          mkdir -p "$HOME/Library/Caches/CocoaPods"
          mkdir -p "$HOME/.pub-cache"
      - name: Build with stable DerivedData path
        run: |
          flutter pub get
          cd ios
          xcodebuild             -workspace Runner.xcworkspace             -scheme Runner             -configuration Release             -destination 'generic/platform=iOS'             -derivedDataPath "$HOME/ci-derived/runner"             CODE_SIGNING_ALLOWED=NO
      - name: Keep pod install deterministic
        run: |
          cd ios
          pod install --deployment

This baseline is enough for many teams to cut CI duration by 30-50% before deeper optimization. For larger orgs, pair it with a dedicated self-hosted runner pool and branch-aware cache keys.

Pre-merge checklist for speed regressions

Run this list whenever iOS CI suddenly gets slower after a Flutter or Xcode update.

  • ☐ Every CI run builds Release/IPA while local compares use Debug
  • ☐ The job runs flutter clean or deletes DerivedData
  • pod install logs show heavy Installing every run
  • ☐ GitHub-hosted runner with no local SSD cache
  • ☐ 16GB machine running multiple jobs or simulators
  • ☐ README-only changes still trigger a full iOS build
  • ☐ Xcode version in CI does not match your local machine

If the first three boxes are checked, fix caches first. If the last two are checked, fix triggers and machine specs. See the disk guide if builds get slower over time.

FAQ

Is GitHub Actions always slower than local Mac builds?

Not always, but for Flutter iOS pipelines it is commonly slower when dependency and DerivedData layers cannot stay warm. Self-hosted runners reduce this gap.

Should I switch immediately to self-hosted runners?

Profile first. If your table shows repeated cold-start penalties, move one workflow to a self-hosted runner and compare ten-run medians before full migration.

Do I need expensive hardware to fix this?

Usually no. A rented Apple Silicon Mac with stable caches often outperforms larger but stateless hosted CI for iOS build workloads.

How do I convince the team this is worth it?

Show the stage timing table, convert saved minutes into developer waiting cost, and include release predictability benefits, not just raw speed.

Read these in order if you are redesigning Flutter iOS CI around predictable xcodebuild performance.

Need stable iOS CI speed on real Apple Silicon?

kvmboot cloud Mac gives you dedicated M-series hosts for Flutter iOS pipelines. Start with daily rent a mac for benchmarking, then keep a monthly self-hosted runner for predictable release cadence.

When your team is ready to move from debugging CI variance to shipping, view cloud Mac plans