iOS and macOS CI benchmark: Avrea is 3-23x faster than GitHub Actions

On GitHub Actions' standard macOS runner, a Firefox for iOS build takes over 12 minutes and on Avrea with a warm cache it takes just under 2 minutes. Mastodon, built with Tuist, goes from 4 minutes 30 seconds to 12 seconds.

iOS CI tends to be slow and/or expensive. Hosted macOS runners tend to be quite small: GitHub's standard macos-26 runner is a 3 vCPU M1 with 7 GB of memory. Their larger 5 vCPU runner is about three times faster and costs 65% more per minute.

We benchmarked a selection of open source apps to measure what makes Apple ecosystem builds fast on Avrea.

What Avrea does for Apple builds

Avrea's macOS runners are Apple M5 Max machines, and they speed up Apple builds with three caching layers. The caches are independent but they work the best when used together.

Xcode compilation cache. Xcode can cache the output of Swift and Clang compile steps, keyed by a hash of its inputs, and fetch it from a remote store instead of compiling again. Avrea stores the output in a colocated cache, so a new build reuses what earlier builds have already compiled. This covers Xcode projects, workspaces and Swift packages, including package dependencies. Apple: build settings reference

Tuist module cache. Tuist generates Xcode projects from Swift manifests. Its cache works a level higher than Xcode's: targets you are not changing are swapped for prebuilt frameworks, so they are neither compiled nor linked. Add a fullHandle like "my-org/my-app" to Tuist.swift use it. Tuist docs: module cache

Swift package registry. SwiftPM normally clones every dependency's git repository. A package registry (SE-0292) serves each version as a single archive instead. Avrea runs one colocated for public GitHub packages and for your private packages. SwiftPM: using a package registry

How we ran the test

We built a few open-source apps at pinned commits: Firefox, Signal, Mastodon (as a plain Xcode project and generated with Tuist, from Tuist's cache-benchmark), Wikipedia and Pocket Casts on iOS, and iTerm2, CotEditor, Sequel Ace and NetNewsWire on macOS.

All times are the build step only: no checkout, no package downloads, and tests are compiled but not run. Avrea numbers are the stock workflow with a warm cache unless a table says no cache. GitHub numbers have no cache.

Here is the shape of the workflow we measured, for Mastodon:

On GitHub the same workflow runs with runs-on: macos-26.

Results

The longer the build time, the more there is to save. Two of the larger open-source iOS apps, Firefox and Signal, take about 12 minutes per build on GitHub's standard runner. Mastodon with Tuist gains the most, as two of our caches apply. NetNewsWire, a small Mac app, gains the least: there isn't much to compile in the first place.

What makes it faster

The machine

If we turn of the caches, the speedup comes from hardware alone:

An M5 Max builds these apps 3.6 to 6.9 times faster than GitHub's standard runner and about 2 times faster than its larger one. The larger runner closes a lot of the gap, but it costs more per minute than Avrea does (see below).

GitHub's times also vary a lot. Mastodon with Tuist took anywhere from 186 to 355 seconds on the standard runner across our runs; on Avrea the same build stayed between 34 and 48 seconds.

The compilation cache

Xcode 26 can cache the output of each compile step. Avrea turns this on for every macOS job and serves the cache from storage colocated with the runners. Cross-machine hits need identical paths (Swift Forums); every Avrea VM builds at the same workspace path, so they match.

Swift packages are cached too. Since Xcode 26.5, Swift package dependencies built as part of an app are cached as well: swift-syntax used as a dependency of a small app builds 2.3 times faster from the cache.

Caching keeps working as main moves. Mastodon, with the cache warmed at the first commit:

One timestamp can switch it off. We noticed that Firefox gains very little from the cache. Turns out Firefox stamps its build time into two generated source files, which invalidates the cache for its main app target and everything that depends on it. Pinning the timestamp makes every cache key reproducible and cuts Swift compile work by 85%:

The cache helps least where the build is dominated by steps it doesn't cover, like Firefox's icon compile, and in small apps like NetNewsWire, where fixed costs make up most of the build. Signal turns off explicitly built modules for its own targets, which switches off Swift caching for them, so only its dependencies come from the cache.

The Tuist module cache

Tuist's module cache works a level higher than Xcode's: whole targets you aren't changing come from the cache as prebuilt frameworks. On Mastodon generated with Tuist:

With both caches warm, a new job builds in 11.8 seconds, against 269.8 seconds on GitHub's standard runner with the same Tuist version: 23 times faster. The new-job result is about 3 seconds slower than the 8.8-second single-run result.

The module cache is filled by a tuist cache warm job on main, and pull requests pick it up:

The package registry

The registry saves time before the build starts: resolving dependencies. Resolving a package's dependencies with no lockfile:

With a committed lockfile and --force-resolved-versions, Vapor resolves in 4 seconds from the registry, against 12 from git. It's not compile time, and it's small in absolute terms, but it's pure waiting on every job. Private packages resolve the same way. These registry measurements cover Vapor and The Composable Architecture. In the latest production tests, registry resolution failed for Firefox, Mastodon and CotEditor; CotEditor took 475.6 seconds using git. We do not report a registry speedup for those three apps.

What it costs

Avrea's 8 vCPU Mac costs 29% more per minute than GitHub's standard runner and 22% less than its larger one. Because builds finish so much faster, every build in our tests still cost less on Avrea: 3.5 to 20 times cheaper than the standard runner, 2.4 to 12.9 times cheaper than the larger one.

Cost per build, build step only.

Against GitHub's larger runner:

* GitHub rounds up to whole minutes for billing.

How to turn it on

Change runs-on to an Avrea macOS label, like avrea-macos-latest-8-vcpu. The compilation cache and the package registry are on for every job, with nothing else to change.

If you use Tuist, it can also reuse whole prebuilt targets from Avrea's module cache. Add a fullHandle to Tuist.swift, and run tuist cache warm on main like the workflow above. For SwiftPM and Tuist package dependencies, pass --replace-scm-with-registry to resolve them from the registry.

The details are in the docs: Xcode compilation cache, Tuist and Swift packages.

What's next

Fast hardware and a warm cache get most apps to a fraction of their GitHub build time. The next steps are about the remaining seconds in a new job: fetching Tuist's cached modules faster, and warming caches before a job even starts. As always, fixing one bottleneck moves it somewhere else.

Appendix: how we measured

Each benchmark job timed the xcodebuild build step with hyperfine: one warmup build, then two or more timed builds on the same machine, at a pinned commit with the same sources and tool versions on every side. Where a project ran more than once, we report the mean of all timed builds.

The warm-cache numbers come from separate jobs: after one job built the commit and warmed the caches, three more new jobs each ran one stock build, alongside one new job with the cache off as a control. We didn't pin hosts, so a job may run on a different machine than the one that warmed the cache. The machine table and the Tuist layer table were timed on the same machine after a warmup build, where Xcode's local cache also helps the layer table.

Signal and Sequel Ace were built with Xcode 26.6, following their own CI; the rest with Xcode 26.5.

Mastodon with Tuist used Tuist 4.208.0 on both sides. The new-VM warm-cache measurements were taken after PR 3006 shipped: 12.2, 12.2 and 11.1 seconds with both caches (mean 11.8 seconds), and 18.6, 18.2 and 17.7 seconds with the module cache only (mean 18.2 seconds). Ratios use the unrounded means.

Tuist's published benchmark. Tuist's own published run of the same Mastodon project, with the module cache only, measured 25.0 seconds on Namespace (M4 Pro, 6 vCPU). Ours is 18.2 seconds (M5 Max, 8 vCPU). Ofc, this is not apples to apples. Tuist's run is the most recent public benchmark of the same project set, published in October 2025 on Xcode 26.0.1 with an older Tuist and the compilation cache off. Ours uses Xcode 26.5 and Tuist 4.208.0 on different hardware. This is included as a reference point, not as a controlled and fair comparison.