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
| Layer | What it skips | Applies to | Setup |
|---|---|---|---|
| Compilation cache | Recompiling unchanged files | Any Xcode 26 build | None |
| Tuist module cache | Compiling and linking unchanged targets | Tuist projects | fullHandle in Tuist.swift |
| Package registry | Cloning dependency git history | Xcode and SwiftPM package dependencies (exact versions) | Xcode: none. SwiftPM and Tuist: --replace-scm-with-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.
| GitHub standard | GitHub larger | Avrea | |
|---|---|---|---|
| Runner label | macos-26 | macos-26-xlarge | avrea-macos-latest-8-vcpu |
| Chip | M1 | M2 Pro | M5 Max |
| vCPU / RAM | 3 / 7 GB | 5 / 14 GB | 8 / 24 GB |
| Xcode | 26.5 | 26.5 | 26.5 |
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:
name: Build
on:
pull_request:
jobs:
build:
runs-on: avrea-macos-latest-8-vcpu
steps:
- uses: actions/checkout@v7
- name: Build
run: >-
xcodebuild
-project Mastodon.xcodeproj
-scheme Mastodon
-destination 'platform=iOS Simulator,name=iPhone 17'
build
On GitHub the same workflow runs with runs-on: macos-26.
Results
| Project | GitHub | GitHub larger | Avrea, warm cache | vs GitHub | vs larger |
|---|---|---|---|---|---|
| Firefox (test build) | 747.6 s | 308.8 s | 118.9 s | 6.3x | 2.6x |
| Signal (test build) | 719.3 s | 235.5 s | 129.8 s | 5.5x | 1.8x |
| Mastodon (Tuist) | 269.8 s | 82.3 s | 11.8 s | 22.8x | 7.0x |
| Mastodon | 279.4 s | 89.3 s | 31.8 s | 8.8x | 2.8x |
| Wikipedia (test build) | 401.4 s | 211.3 s | 43.8 s | 9.2x | 4.8x |
| Wikipedia | 159.2 s | 70.6 s | 32.7 s | 4.9x | 2.2x |
| Pocket Casts | 554.0 s | 206.9 s | 73.6 s | 7.5x | 2.8x |
| iTerm2 (macOS) | 386.7 s | 126.6 s | 29.6 s | 13.1x | 4.3x |
| CotEditor (macOS, test build) | 192.5 s | 88.0 s | 25.8 s | 7.5x | 3.4x |
| Sequel Ace (macOS, test build) | 232.6 s | 88.9 s | 26.6 s | 8.7x | 3.3x |
| NetNewsWire (macOS) | 81.3 s | 44.6 s | 26.5 s | 3.1x | 1.7x |
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 off the caches, the speedup comes from hardware alone:
| Project | GitHub | GitHub larger | Avrea | vs GitHub | vs larger |
|---|---|---|---|---|---|
| Firefox (test build) | 747.6 s | 308.8 s | 139.6 s | 5.4x | 2.2x |
| Signal (test build) | 719.3 s | 235.5 s | 122.0 s | 5.9x | 1.9x |
| Mastodon (Tuist) | 269.8 s | 82.3 s | 39.1 s | 6.9x | 2.1x |
| Mastodon | 279.4 s | 89.3 s | 45.3 s | 6.2x | 2.0x |
| Wikipedia (test build) | 401.4 s | 211.3 s | 98.5 s | 4.1x | 2.1x |
| Wikipedia | 159.2 s | 70.6 s | 35.9 s | 4.4x | 2.0x |
| Pocket Casts | 554.0 s | 206.9 s | 101.2 s | 5.5x | 2.0x |
| iTerm2 (macOS) | 386.7 s | 126.6 s | 66.1 s | 5.9x | 1.9x |
| CotEditor (macOS, test build) | 192.5 s | 88.0 s | 45.8 s | 4.2x | 1.9x |
| Sequel Ace (macOS, test build) | 232.6 s | 88.9 s | 34.7 s | 6.7x | 2.6x |
| NetNewsWire (macOS) | 81.3 s | 44.6 s | 22.4 s | 3.6x | 2.0x |
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.
| Project | No cache | Warm cache | Speedup |
|---|---|---|---|
| Mastodon | 61.2 s | 31.8 s | 1.9x |
| Wikipedia (test build) | 113.4 s | 43.8 s | 2.6x |
| Wikipedia | 39.0 s | 32.7 s | 1.2x |
| Pocket Casts | 98.8 s | 73.6 s | 1.3x |
| iTerm2 (macOS) | 63.0 s | 29.6 s | 2.1x |
| CotEditor (macOS, test build) | 46.6 s | 25.8 s | 1.8x |
| Sequel Ace (macOS, test build) | 38.2 s | 26.6 s | 1.4x |
| NetNewsWire (macOS) | 31.3 s | 26.5 s | 1.2x |
| Firefox (test build) | 137.6 s | 118.9 s | 1.2x |
| Signal (test build) | 146.4 s | 129.8 s | 1.1x |
| swift-syntax in an app | 15.4 s | 6.6 s | 2.3x |
| swift-syntax (package) | 14.8 s | 11.1 s | 1.3x |
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:
| Commit | No cache | Warm cache |
|---|---|---|
| Cached commit | 54.0 s | 29.6 s |
| 10 commits later (22 files changed) | 49.5 s | 34.2 s |
| 50 commits later (62 files changed) | 47.6 s | 34.1 s |
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%:
| Firefox, warm cache | As shipped | Timestamp pinned |
|---|---|---|
| Swift compile work | 256-266 s | 40-47 s |
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:
| Layers | Time |
|---|---|
| Nothing | 36.8 s |
| Compilation cache | 21.9 s |
| Module cache | 14.4 s |
| Both | 8.8 s |
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:
name: Warm Tuist caches
on:
push:
branches: [main]
jobs:
warm:
runs-on: avrea-macos-latest-8-vcpu
steps:
- uses: actions/checkout@v7
- name: Install Tuist
uses: jdx/mise-action@v4
- name: Warm Tuist module cache
run: tuist cache warm
The package registry
The registry saves time before the build starts: resolving dependencies. Resolving a package's dependencies with no lockfile:
| Package | GitHub, git | Avrea, git | Avrea, registry |
|---|---|---|---|
| Vapor (28 packages) | 47.3 s | 23.3 s | 11.0 s |
| The Composable Architecture | 38.3 s | 20.6 s | 7.5 s |
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
| Runner | Hardware | Price per minute |
|---|---|---|
| GitHub macos-26 | M1, 3 vCPU, 7 GB | $0.062 |
| GitHub macos-26-xlarge | M2 Pro, 5 vCPU | $0.102 |
| Avrea macOS 8 vCPU | M5 Max, 8 vCPU, 24 GB | $0.08 |
| Avrea macOS 16 vCPU | M5 Max, 16 vCPU, 48 GB | $0.16 |
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.
| Project | GitHub | GitHub cost* | Avrea | Avrea cost | Faster | Cheaper |
|---|---|---|---|---|---|---|
| Firefox (test build) | 747.6 s | $0.806 | 118.9 s | $0.159 | 6.3x | 5.1x |
| Signal (test build) | 719.3 s | $0.744 | 129.8 s | $0.173 | 5.5x | 4.3x |
| Mastodon (Tuist) | 269.8 s | $0.310 | 11.8 s | $0.016 | 22.8x | 19.6x |
| Mastodon | 279.4 s | $0.310 | 31.8 s | $0.042 | 8.8x | 7.3x |
| Wikipedia (test build) | 401.4 s | $0.434 | 43.8 s | $0.058 | 9.2x | 7.4x |
| Wikipedia | 159.2 s | $0.186 | 32.7 s | $0.044 | 4.9x | 4.3x |
| Pocket Casts | 554.0 s | $0.620 | 73.6 s | $0.098 | 7.5x | 6.3x |
| iTerm2 (macOS) | 386.7 s | $0.434 | 29.6 s | $0.039 | 13.1x | 11.0x |
| CotEditor (macOS) | 192.5 s | $0.248 | 25.8 s | $0.034 | 7.5x | 7.2x |
| Sequel Ace (macOS) | 232.6 s | $0.248 | 26.6 s | $0.035 | 8.7x | 7.0x |
| NetNewsWire (macOS) | 81.3 s | $0.124 | 26.5 s | $0.035 | 3.1x | 3.5x |
Against GitHub's larger runner:
| Project | GitHub larger | Cost* | Avrea | Avrea cost | Faster | Cheaper |
|---|---|---|---|---|---|---|
| Firefox (test build) | 308.8 s | $0.612 | 118.9 s | $0.159 | 2.6x | 3.9x |
| Signal (test build) | 235.5 s | $0.408 | 129.8 s | $0.173 | 1.8x | 2.4x |
| Mastodon (Tuist) | 82.3 s | $0.204 | 11.8 s | $0.016 | 7.0x | 12.9x |
| Mastodon | 89.3 s | $0.204 | 31.8 s | $0.042 | 2.8x | 4.8x |
| Wikipedia (test build) | 211.3 s | $0.408 | 43.8 s | $0.058 | 4.8x | 7.0x |
| Wikipedia | 70.6 s | $0.204 | 32.7 s | $0.044 | 2.2x | 4.7x |
| Pocket Casts | 206.9 s | $0.408 | 73.6 s | $0.098 | 2.8x | 4.2x |
| iTerm2 (macOS) | 126.6 s | $0.306 | 29.6 s | $0.039 | 4.3x | 7.8x |
| CotEditor (macOS) | 88.0 s | $0.204 | 25.8 s | $0.034 | 3.4x | 5.9x |
| Sequel Ace (macOS) | 88.9 s | $0.204 | 26.6 s | $0.035 | 3.3x | 5.8x |
| NetNewsWire (macOS) | 44.6 s | $0.102 | 26.5 s | $0.035 | 1.7x | 2.9x |
* 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.





