Guide

Cutting a GitHub Actions macOS bill, step by step

Most macOS bills on GitHub Actions are inflated by minutes that do nothing for the team. Rebuilding caches. Running matrices nobody reads. Finishing jobs for commits that have already been replaced. This is a checklist of changes ordered by how much they save for how little work, with the YAML where it helps.

1. Cancel runs that are already stale

When someone pushes twice in a minute, the first run is wasted. One block per workflow stops it:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

2. Move everything that is not a build off macOS

Linting, formatting checks, release notes and Slack notifications all run on a Linux runner at a tenth of the price. So do unit tests for shared Swift packages that do not touch UIKit. Split the workflow into a Linux job and a macOS job that depends on it, so a lint failure never spends a macOS minute.

3. Cache the paths that actually cost time

On hosted runners every job starts empty. Restore the expensive directories with a key tied to your lockfiles:

- uses: actions/cache@v4
  with:
    path: |
      ~/Library/Developer/Xcode/DerivedData
      ~/Library/Caches/org.swift.swiftpm
      ~/.cocoapods
      Pods
    key: ${{ runner.os }}-${{ hashFiles('**/Package.resolved', '**/Podfile.lock') }}

DerivedData is the big one and the most fragile; if incremental builds behave oddly, drop it from the cache and keep the package caches.

4. Run the full matrix only on main

A pull request rarely needs five simulators. Build one configuration on pull requests and the full device matrix on merges to main or on a nightly schedule.

5. Filter by path

A change to the README should not trigger an iOS build. Use paths-ignore on the workflow trigger for docs, scripts and anything outside the app target.

6. Set timeouts

A hung simulator can bill the full six-hour default. Set timeout-minutes on every macOS job to a little over your normal build time.

7. Stop reinstalling tools

Installing CocoaPods, fastlane or a specific Xcode with every job burns minutes before the build starts. Pin to what the hosted image already has, or cache the install.

8. Trim artifact retention

Archives and test bundles kept for 90 days add storage charges that creep up. Seven days is plenty for anything not shipped to the store.

9. Above the break-even, stop paying per minute

Everything above shaves the bill. Once you are steadily past about 1,920 macOS minutes a month, no amount of shaving beats a flat price. A dedicated M4 at $119 a month runs unlimited minutes with warm caches. Items 3 and 7 stop mattering entirely. The calculator shows where your team sits.

Frequently asked questions

What is the single biggest saving on a GitHub Actions macOS bill?

+

For most teams, cancelling superseded runs with a concurrency group and moving non-build work to Linux. Together they often remove a third of billed macOS minutes with two small YAML changes.

Is caching DerivedData on GitHub Actions worth it?

+

Often, but it is the fragile one. Package caches (SwiftPM, CocoaPods) are safe and save minutes every job. DerivedData saves more but can cause odd incremental-build failures. Key it tightly and be ready to drop it.

Should I run my device matrix on every pull request?

+

Usually not. One configuration on pull requests and the full matrix on main or nightly catches the same regressions at a fraction of the minutes.

When does none of this matter?

+

Above roughly 1,920 macOS minutes a month, a flat-price dedicated Mac at $119 is cheaper than metered billing. That holds however well the metered setup is optimised. Warm caches remove most of the waste on their own.

Related guides