Guides / Reduce macOS/Windows CI spend

Runner mismatch

Reduce macOS and Windows CI spend

By Keith Mazanec, Founder, CostOps ยท Updated February 2, 2026

A developer adds macos-latest to the CI matrix so tests run on all three platforms. Every PR now spins up a macOS runner at $0.062/min, 10x the Linux rate, to run the same lint and unit tests that have nothing to do with macOS. Multiply that across 20 PRs a day and the bill adds up fast. The fix is straightforward: run platform-independent work on Linux, and reserve macOS and Windows runners for jobs that actually need them.

Symptoms

How to tell if expensive runners are inflating your bill

Check your GitHub Actions usage breakdown by runner type. If macOS or Windows dominate spend but most of the jobs running there are platform-independent, you're overpaying.

  • macOS/Windows dominate your bill. More than 40% of your total CI cost comes from macOS or Windows runners, even though most of your codebase is platform-independent. Linting, formatting, unit tests, and dependency checks rarely need anything beyond Linux.

  • Short jobs on expensive runners. Jobs that finish in under 2 minutes (lint, format, typecheck) are running on macos-latest or windows-latest. GitHub rounds up to the nearest minute, so a 30-second macOS job costs a full minute at $0.062.

  • Full cross-platform matrix on every PR. Your matrix includes os: [ubuntu-latest, macos-latest, windows-latest] and runs on every push. Feature branch pushes trigger macOS and Windows builds for code that will be tested again at merge anyway.

  • Free tier burns out fast. On the Free plan, you get 2,000 included minutes per month. macOS consumes those at 10x: 1 macOS minute = 10 included minutes deducted. That means you effectively get only 200 macOS minutes per month before overages kick in.

Metrics

What macOS and Windows runners actually cost

Consider a cross-platform project running a 3-OS matrix (Linux, macOS, Windows) on every PR. Each platform runs the same test suite for 12 minutes. The team merges 8 PRs per day, and each PR averages 3 pushes.

Before: full matrix on every PR

Runs/day 24
Minutes/run (3 OS) 36
Linux cost/mo $31
Windows cost/mo $52
macOS cost/mo $322
Monthly cost $405/mo

24 runs × 12 min × 22 days × per-OS rate

After: Linux on PRs, full matrix on main

PR runs/day (Linux only) 24
Main runs/day (3 OS) 8
Linux cost/mo $44
Windows cost/mo $17
macOS cost/mo $107
Monthly cost $168/mo

Save $237/mo · $2,844/year · 59% reduction

The math: PR runs drop from 3 OS to 1 (Linux), saving the macOS and Windows legs entirely. Main-branch runs still cover all three platforms but only fire once per merge (8/day) instead of on every push (24/day). The macOS savings alone account for $215/mo of the reduction, because at $0.062/min even modest minute reductions have outsized impact.


Fix 1

Move platform-independent jobs to Linux

Most CI jobs do not need macOS or Windows. Linting, formatting, typechecking, dependency audits, code coverage, and static analysis all produce identical results on Linux. If the job does not use Xcode, the iOS Simulator, Windows-specific APIs, or a platform-native build toolchain, it belongs on ubuntu-latest.

Even tools associated with Apple development can run on Linux. Swift is preinstalled on Ubuntu runners, and tools like SwiftLint and SwiftFormat support Linux. Only jobs that require platform-specific SDKs (Xcode, .NET for Windows, Win32 APIs) need to stay on expensive runners.

$0.062/min for lint
jobs:
  lint:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

# Lint has no macOS dependency.
# Paying 10x for the same result.
$0.006/min for lint
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

# Same result, 10x cheaper.

A 2-minute lint job on macOS costs $0.124 per run. On Linux, the same job costs $0.012. Over 500 runs/month, that single job shift saves $56/mo. Audit every job in your workflow and ask: does this job use a platform-specific SDK? If not, move it to Linux. For trivial jobs (labeling, notifications), you can go even cheaper with ubuntu-slim at $0.002/min. See overpowered runners for lightweight jobs for runner-sizing strategies beyond OS selection.

Fix 2

Gate cross-platform matrix to main and release branches

If your project needs cross-platform testing, it does not need it on every push to every feature branch. Platform-specific bugs are rare; most failures are logic errors that surface identically on Linux. Run the full OS matrix only on main and release branches, where you need final validation before shipping. On PRs, run Linux only.

Use the matrix exclude pattern with a conditional expression to automatically skip macOS and Windows legs on feature branches:

.github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        isMain:
          - ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
        exclude:
          # Skip macOS on feature branches
          - os: macos-latest
            isMain: false
          # Skip Windows on feature branches
          - os: windows-latest
            isMain: false
      fail-fast: true
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

On PRs, the matrix evaluates isMain as false, and the exclude rules remove the macOS and Windows entries. Only ubuntu-latest runs. On pushes to main, isMain evaluates to true, so nothing is excluded and all three platforms run.

One caveat: if you need required status checks that match specific matrix combinations (e.g., test (macos-latest, ...)), those checks will not exist on PRs and branch protection may block merging. Either make only the Linux check required, or use a separate workflow for the full matrix that triggers on push to main only.

Fix 3

Split platform-specific tests into a gated workflow

For finer control, move macOS and Windows testing into a separate workflow that only triggers on main, release branches, or tags. This keeps your PR workflow fast and cheap while still validating platform compatibility before release.

.github/workflows/ci.yml (PR workflow)
name: CI

on:
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test
.github/workflows/cross-platform.yml (main only)
name: Cross-Platform Tests

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

This pattern gives you clear separation: the PR workflow is your fast feedback loop (Linux only, sub-5-minute), and the cross-platform workflow is your safety net (all three OSes, runs only on merged code). If a platform-specific bug slips through, you catch it immediately after merge instead of on every PR push.

Fix 4

Set explicit timeouts on expensive runners

The default timeout-minutes in GitHub Actions is 360 (6 hours). A single stuck macOS job at the default timeout would consume 360 minutes at $0.062/min, costing $22.32 for one failed run. Against included minutes, that is 3,600 minutes deducted (10x multiplier), which burns through nearly the entire Team plan quota.

Always set explicit timeouts, especially on macOS and Windows jobs. Base the timeout on the job's actual p90 runtime plus a small buffer.

.github/workflows/ci.yml
jobs:
  test-macos:
    runs-on: macos-latest
    timeout-minutes: 20   # p90 is ~12 min; cap at 20
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  test-windows:
    runs-on: windows-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

Setting timeout-minutes: 20 on a macOS job caps worst-case cost at $1.24 per run instead of $22.32. On the Free plan, that caps the included-minutes damage at 200 instead of 3,600.


Reference

GitHub Actions runner rates by OS

These are the current standard hosted runner rates as of January 2026. Larger runners (4+ cores) are available on Team and Enterprise Cloud plans at higher per-minute rates but are always billed per-minute (included plan minutes cannot be used).

Runner Rate Multiplier Cost/hr
Linux 2-core (x64) $0.006/min 1x $0.36
Linux 2-core (arm64) $0.005/min 0.8x $0.30
Windows 2-core $0.010/min 2x $0.60
macOS (M1/Intel) $0.062/min 10x $3.72
macOS 12-core (Intel) $0.077/min n/a $4.62
macOS M2 Pro (5-core) $0.102/min n/a $6.12

Larger runners (macOS 12-core, M2 Pro) are only available on Team and Enterprise Cloud plans. They are always billed per-minute; included plan minutes do not apply. Free tier included minutes by plan: 2,000/mo (Free), 3,000/mo (Team/Pro), 50,000/mo (Enterprise). macOS deducts included minutes at 10x; Windows at 2x.

Team / Enterprise Cloud Larger runners require GitHub Team or Enterprise Cloud. Standard runners are available on all plans.

Reference

Free tier impact by runner OS

The included minutes on each plan are consumed at different rates depending on the runner OS. Here is how many effective minutes you get per OS before overages start:

Plan Included Linux Windows macOS
Free 2,000 2,000 min 1,000 min 200 min
Team/Pro 3,000 3,000 min 1,500 min 300 min
Enterprise 50,000 50,000 min 25,000 min 5,000 min

On the Free plan, a team running 10 minutes of macOS CI per day exhausts their entire monthly allocation in 20 days (10 min × 10x × 20 days = 2,000). Moving those jobs to Linux would consume only 200 of the 2,000 included minutes over the same period.

Related guides

Guides / Reduce macOS/Windows CI spend

See which runners are eating your budget

CostOps breaks down spend by runner OS and flags jobs where macOS or Windows runners are used for platform-independent work.

Free for 1 repo. No credit card. No code access.

Built by engineers who've managed CI spend at scale.