Back to all blogs

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

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.

01 October 2026

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.