# How to reduce GitHub Actions costs

> Seven changes that reduce GitHub Actions cost, ranked by what each one saves on the same worked month, with the before and after YAML for every one.

Source: https://latchkey.dev/learn/cost/reduce-github-actions-costs  
Updated: 2026-09-20

To reduce GitHub Actions cost, change what runs and where it runs, in that order: minutes you never spend are cheaper than minutes you spend on a cheaper machine. This page ranks seven changes by what each one saves on one worked month, priced at the rates GitHub published on 20 September 2026, and gives the before and after YAML for every one.

Every tip list for this topic opens with caching. Caching is here too, near the bottom, because on a bill made of expensive minutes it is not where the money is.

The month used throughout: 21,000 macOS minutes, 40,000 Linux and 4,000 Windows, in private repositories, on a GitHub Team plan. At list rates that is $1,302 plus $240 plus $40, which is $1,582, less the 3,000 included minutes spent on the most expensive work: an invoice of $1,396. Each saving below is that change alone against this month, so they overlap rather than add.

## 1. Move everything that does not need macOS onto Linux. Saves $588

A macOS minute is $0.062 and a Linux minute is $0.006, so every minute you move is worth 5.6 cents. Half of a typical iOS pipeline, the linting, the dependency audits, the JavaScript bundle, anything that only reads source text, never needs Xcode.

Move 10,500 of the 21,000 macOS minutes and you save 10,500 times $0.056, which is $588 a month, and the pipeline does what it did before. The same logic applies to an OS matrix that fans out to three platforms on every pull request: run the full matrix on the default branch and on a schedule, and Linux alone on pull requests.

It is the largest change most teams have, and the one carrying least risk. [macOS runner cost](/learn/cost/github-actions-macos-runner-cost) works through the mobile case.

```.github/workflows/test.yml
# before: every push fans out to three operating systems
jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    runs-on: ${{ matrix.os }}

# after: pull requests get Linux, the full matrix runs where it earns its keep
jobs:
  test:
    strategy:
      matrix:
        os: ${{ github.event_name == 'pull_request' && fromJSON('["ubuntu-latest"]') || fromJSON('["ubuntu-latest","windows-latest","macos-latest"]') }}
    runs-on: ${{ matrix.os }}
```

## 2. Do not start the expensive job when nothing it covers changed. Saves $163

Workflows are triggered by pushes, not by relevance. A pull request that edits a README starts the same iOS build as one rewriting the networking layer, and bills the same.

Gate the expensive job on paths. If one push in eight touches only documentation, that is 2,625 of those 21,000 macOS minutes never spent, or $162.75 a month. Check the ratio first: `git log --name-only` over the last month shows how many commits touched nothing but text.

If a required status check is involved, gate the work inside the job rather than skipping the job, so the check still reports.

```.github/workflows/ios.yml
# before: the iOS workflow runs on every push, whatever changed
on:
  pull_request:

# after: not on documentation-only changes
on:
  pull_request:
    paths-ignore:
      - "docs/**"
      - "**/*.md"
      - ".github/ISSUE_TEMPLATE/**"
```

## 3. Cancel the run that a newer push already made pointless. Saves $158

Push twice in five minutes and GitHub runs the workflow twice. The first is obsolete the moment the second starts, and you pay for it to finish.

A concurrency group keyed on the branch, with `cancel-in-progress`, kills the superseded run the instant the new one starts. If 10 percent of your minutes belong to runs nobody was waiting for, that is 10 percent of $1,582, or $158. Measure yours: superseded runs appear in the usage report as canceled runs with billed minutes behind them.

Key the group on the branch, never on the workflow alone, or two unrelated branches cancel each other, and leave the default branch out of it if you deploy from there.

```.github/workflows/ci.yml
# before: nothing, which means every push runs to completion

# after: a newer push cancels the run it replaced
concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
```

## 4. Put the standard runner back where a larger one is doing nothing. Saves $60 per 10,000 minutes

Larger runners are billed from the first minute, cannot draw on included minutes, and cost roughly double per doubling of cores: $0.012 at 4 cores against $0.006 at 2. A job bound by network calls, a single-threaded compiler or a database does not run faster on more cores, so it costs twice as much for the same wall clock.

Ten thousand minutes a month on a 4-core larger runner is $120; the same job on a standard 2-core runner is $60. Run it once at each size and compare the durations in the run summary: if it did not halve, the larger runner is losing money.

The inverse holds too: a job that does halve is free to move up, because the rate and the time cancel out and you get the answer sooner.

```.github/workflows/build.yml
# before: larger by default, chosen once and never measured
jobs:
  build:
    runs-on: ubuntu-latest-4-cores

# after: standard, with a timeout that proves the duration claim
jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 20
```

## 5. Run Linux on arm64 where the toolchain already supports it. Saves $40

GitHub prices arm64 below x64 at every size, the only place in the price list where the cheaper option is the newer one. Standard Linux arm64 is $0.005 against $0.006, and the gap widens on larger runners: $0.008 against $0.012 at 4 cores, $0.098 against $0.162 at 64, a third off the compute line.

On this month, moving all 40,000 Linux minutes to standard arm64 turns $240 into $200, and on larger runners the same change is worth several times that.

The catch is native dependencies. Anything shipping prebuilt x86 binaries, and any Docker image without an arm64 variant, either falls back to emulation, which is slower and so more expensive, or fails outright. Move one workflow first and read the timings.

```.github/workflows/ci.yml
# before
runs-on: ubuntu-latest

# after: the arm64 standard runner, $0.005 a minute against $0.006
runs-on: ubuntu-24.04-arm
```

## 6. Cap the runaway job. Saves $22.32 every time a macOS job hangs

A job with no `timeout-minutes` runs until GitHub stops it, and the default ceiling is 360 minutes. On macOS one hung job is 360 minutes at $0.062, which is $22.32; on Linux it is $2.16. Cheapest insurance in the product, and almost nobody sets it.

Set a timeout on every job at about twice its normal duration, and a shorter one on the steps that hang rather than fail: network installs, simulator boots, anything waiting on a lock.

Look too at the jobs that fail for reasons that have nothing to do with your code, because those minutes are billed twice: once for the failure, once for the re-run. [Exit code 137 in GitHub Actions](/learn/failures/exit-code-137-in-github-actions) and [no space left on device](/learn/failures/no-space-left-on-device-github-actions) are the usual two.

```.github/workflows/ios.yml
# before: no ceiling, so a hung step bills 360 minutes
jobs:
  test:
    runs-on: macos-latest

# after: the job and the step that hangs both have a ceiling
jobs:
  test:
    runs-on: macos-latest
    timeout-minutes: 30
    steps:
      - run: xcodebuild -scheme App test
        timeout-minutes: 20
```

## 7. Cache the work, then keep the storage bill honest

Caching is last because it saves minutes rather than rate, and a Linux minute is $0.006. A cache that turns a 90 second `npm ci` into a 20 second restore saves 70 seconds a run: across 840 runs that is 16 hours of machine time, about $6 on Linux and $60 on macOS. An order of magnitude below the first change here.

It is also not free. Every repository gets 10 GB of Actions cache, measured at peak usage each hour, and anything above that is $0.07 per GB-month. Artifacts cost $0.25 per GB-month above a shared allowance and accrue hourly, so deleting them later stops future charges without refunding the hours already recorded.

Cache the expensive restores, and set a short retention on artifacts you need for a day. The default is 90 days.

```.github/workflows/ci.yml
# before: no cache, and artifacts kept for the 90 day default
- uses: actions/setup-node@v7
  with:
    node-version: 22
- run: npm ci
- uses: actions/upload-artifact@v7
  with:
    name: build
    path: dist

# after: the dependency cache the setup action already ships, and a short retention
- uses: actions/setup-node@v7
  with:
    node-version: 22
    cache: npm
- run: npm ci
- uses: actions/upload-artifact@v7
  with:
    name: build
    path: dist
    retention-days: 5
```

## What is left after all seven

Apply the first three to the worked month and the invoice falls from $1,396 to about $600, without touching a test or a deployment. What remains is the rate, and there are three ways to change it: a smaller machine, a different architecture, or a different runner.

The third is the only lever left once the workflow is lean. Latchkey publishes $0.0025 a minute at 2 vCPU against GitHub's $0.006, so the Linux part of a trimmed month costs 58 percent less. It sells no macOS or Windows runners, so those minutes stay where they are. [How Latchkey cuts the GitHub Actions bill](/github-actions-cost-reduction) is that case in full.

Price your own month at both rates with the [GitHub Actions cost calculator](/learn/cost/github-actions-cost-calculator), read [how GitHub Actions pricing works](/learn/cost/github-actions-pricing-explained) for the rules behind the rates, and if your own hardware is on the table, [what a self-hosted runner really costs](/learn/cost/self-hosted-github-runner-total-cost) has the break-even.

## FAQ

### How can I reduce GitHub Actions costs without rewriting my workflows?

Three edits to the workflow files, none of them to what the workflows run: a concurrency group with cancel-in-progress, paths-ignore on the expensive triggers, and timeout-minutes on every job. On the month above, the first two are worth about $320 between them.

### How do I find the workflows that cost the most?

Download the detailed usage report from your billing settings rather than reading the dashboard. It breaks usage down by workflow and runner type, the only view that separates the macOS lines. Sort by minutes multiplied by that runner's rate, not by minutes, or you will spend a week optimizing a Linux job that costs a tenth of an iOS one.

### Are GitHub larger runners worth it?

Only when the job genuinely parallelizes. The rate roughly doubles for each doubling of cores, so a larger runner is cost neutral when it halves the wall clock and a loss when it does not. It also cannot draw on included minutes and is billed on public repositories, which makes the first bill larger than people expect.

### How do I set a GitHub Actions budget?

In your account billing settings, where budgets and alerts are set per metered product. GitHub will also email you when included usage passes 90 and 100 percent of the allowance, and an account with no payment method has usage blocked once the quota is gone. A budget caps the damage; the changes above remove the cause.

## References

- [GitHub: Actions runner pricing, every per-minute rate by runner (verified 2026-09-20)](https://docs.github.com/en/billing/reference/actions-runner-pricing)
- [GitHub Actions billing: included minutes, storage accrual and rounding (verified 2026-09-20)](https://docs.github.com/en/billing/managing-billing-for-your-products/about-billing-for-github-actions)
- [GitHub Actions: concurrency, cancel-in-progress and job timeouts (verified 2026-09-20)](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax)
- [Latchkey pricing: published per-minute runner rates (verified 2026-09-20)](https://latchkey.dev/pricing)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
