# Why is my CI so expensive? A diagnostic

> Why is my CI so expensive: six shapes of GitHub Actions invoice, what each one usually means, and the single check that confirms or clears each cause.

Source: https://latchkey.dev/learn/cost/why-is-my-ci-so-expensive  
Updated: 2026-09-21

Why is my CI so expensive is almost never a mystery once you know which of two meters moved: the rate you are paying per minute, or the number of minutes you are buying. This page names six shapes of GitHub Actions invoice, says what each one usually is, and gives the one check that confirms or clears it before you change any workflow.

A CI bill has exactly two inputs, and every cause below is one of them wearing a different hat. Either the work got more expensive per minute, which is a runner type or a runner size, or there is more work, which is triggers, retries or storage.

Work down the list in order. It is ordered by how much money each cause typically accounts for, not by how likely it is, and the first two together explain most invoices that doubled without anyone deploying anything unusual.

## Start by asking which meter moved

Pull the usage report and group it by SKU. If one SKU grew, the rate moved: somebody added a macOS or Windows leg, or switched a job to a larger runner. If every SKU grew together, the volume moved: more triggers, more retries, or more branches. That single grouping saves an afternoon of guessing, and it is the only view that separates the two.

Then check the date the growth started against your own history. A bill that stepped on a specific day is a change somebody made; a bill that has been climbing for six months is your repository growing. The first has a commit behind it and the second does not, and they need entirely different answers.

| What the invoice looks like | What it usually is | The check that settles it |
| --- | --- | --- |
| Roughly ten times a Linux baseline | macOS minutes | Group the usage report by SKU and look for the macOS line |
| Stepped up on one specific day | A larger runner was switched on | Search the workflows for a size label, and check whether the allowance stopped covering it |
| Climbs with the size of the team | More pushes, no caching, no filters | Count runs per week against merged pull requests per week |
| Minutes high, wall clock low | A matrix of short jobs and per-minute rounding | Compare billed minutes against summed job durations |
| A sizeable share of runs are retries | Flaky or transient failures | Count runs with `run_attempt` above 1 |
| Grew without any minutes changing | Artifact and cache storage | Look for the storage SKUs rather than the Actions minutes SKU |

> SKU names and report fields from [GitHub billing reports](https://docs.github.com/en/billing/reference/billing-reports), read 21 September 2026.

## Cause one: the machine, not the workload

A macOS minute is $0.062 and a standard Linux minute is $0.006, so one iOS job is worth ten of anything else on the invoice. Windows is $0.010, a gentler premium that is nonetheless two thirds more than Linux for work that frequently did not need Windows in the first place. If the macOS or Windows line is the tall one, the answer is not to make those jobs faster, it is to run fewer of them.

Larger runners are the version of this that appears overnight. They cost roughly double per doubling of cores, they cannot draw on your included minutes at all, and they are billed on public repositories where standard runners are free. A single pull request that changed `runs-on` to a 16-core label can produce a step change in the bill with no other visible cause, and the allowance disappearing at the same time makes the step look larger than the rate difference alone.

Confirm by reading the label, not by reading the workflow name. [GitHub Actions larger runners pricing](/learn/cost/github-actions-larger-runners-pricing) has every size and rate, and [Windows runner cost](/learn/cost/github-actions-windows-runner-cost) prices the matrix leg most teams have forgotten they added.

## Cause two: work you did not need to start

Workflows are triggered by pushes, not by relevance. A pull request that edits a README starts the same build as one rewriting the networking layer, and it bills the same. Push twice within five minutes and the first run keeps going, obsolete and fully charged, unless something cancels it.

These are volume causes, so they scale with the team rather than with the product, which is why they show up as a bill that climbs steadily rather than one that steps. The shape to look for is runs per week growing faster than merged pull requests per week.

The fixes are three lines of YAML and they are priced, against one worked month, in [how to reduce GitHub Actions costs](/learn/cost/reduce-github-actions-costs). This page deliberately does not repeat them, because a tactic list and a diagnostic are different documents and splitting one answer across two pages helps nobody.

## Cause three: the same work, billed twice

A retry buys the same minutes again. If a meaningful share of your runs are second attempts, that share of the bill produced no additional information about your code, and it is the one cause on this page that no per-minute discount touches: a 30 percent cheaper runner applied to a pipeline that re-runs 15 percent of its jobs saves less than fixing the flakiness would.

Separate the two kinds before you act. A test that fails on real non-determinism is your code and needs a fix in the test. A job that failed on a registry timeout, a DNS failure or a lost runner is not your code at all, and those minutes are pure waste in both directions: you pay for the failure, you pay for the retry, and the pipeline sits red until a human presses a button.

Count it rather than estimating it. The command is in [identifying high cost GitHub Actions workflows](/learn/cost/measure-github-actions-costs), and it is three lines.

## Cause four: rounding, and cause five: storage

GitHub rounds the minutes and partial minutes each job uses up to the nearest whole minute. A twelve-leg matrix of 40-second jobs uses eight minutes of machine time and bills twelve, and the gap is invisible in every dashboard that reports duration. If your billed minutes are well above the sum of your job durations, rounding is the difference and the fix is fewer, longer jobs.

Storage is the cause that produces a bill with no minutes behind it at all. Artifacts and packages share an allowance and cost $0.25 per GB-month beyond it; Actions cache has its own 10 GB per repository and costs $0.07 per GB-month above that. Both accrue hourly, which is the part that catches people: deleting an artifact stops future charges and does not refund the hours already recorded.

A retention policy is usually the whole fix, and the default is generous: artifacts and logs are retained for 90 days unless you say otherwise. [Cache and artifact storage cost](/learn/cost/github-actions-cache-and-artifact-storage-cost) works through the arithmetic and the retention change that lands on 1 October 2026.

## Cause six: nothing changed, and that is the answer

Sometimes the bill is correct and the expectation was wrong. Included minutes reset monthly and are counted in minutes rather than dollars, so a month that happened to spend the allowance on Linux looks cheap and a month that spent it on macOS looks expensive for the same work. A plan change, a new organisation, or a repository moving from public to private will all produce a step with no workflow change behind it.

Private is the big one. Standard runners are free and unlimited on public repositories, so a repository that goes private starts paying full rate for work that was genuinely free the week before, and nothing in the workflow files will show you why.

Once you know which of the six it is, the order of operations is the same every time: stop the work you did not need, then move the work that is on the wrong machine, then and only then argue about the rate. [Running out of minutes](/learn/cost/github-actions-out-of-minutes) covers what happens when the allowance goes rather than when the invoice grows.

## FAQ

### Why did my GitHub Actions bill suddenly double?

A step on a specific date is nearly always a runner change: a macOS or Windows leg added to a matrix, or a job moved to a larger runner. Larger runners double roughly per doubling of cores and also stop drawing on your included minutes, so the step is bigger than the rate difference alone would suggest.

### Why is my CI expensive when the jobs are short?

Per-minute rounding. Every job is rounded up to the nearest whole minute, so a wide matrix of 40-second jobs bills roughly a minute and a half for every minute it actually runs. Compare your billed minutes against the sum of your job durations; if there is a large gap, consolidating legs recovers most of it.

### Can a cheaper runner fix an expensive CI bill?

It multiplies whatever is left after the other five causes, which makes it the last change rather than the first. It does nothing at all about re-runs, storage, or jobs that should never have started, and those three account for most invoices that surprised somebody.

### My repository is public. Why am I being charged for Actions at all?

Standard runners are free on public repositories, but larger runners are always charged, even there and even when you have allowance left. Storage is the other possibility: artifacts and caches are billed by the GB-month regardless of what kind of repository produced them.

## References

- [GitHub: Actions billing, allowances, rounding and storage accrual (verified 2026-09-21)](https://docs.github.com/en/billing/concepts/product-billing/github-actions)
- [GitHub: Actions runner pricing, every per-minute rate by runner (verified 2026-09-21)](https://docs.github.com/en/billing/reference/actions-runner-pricing)
- [GitHub billing reports: grouping usage by SKU, repository and workflow (verified 2026-09-21)](https://docs.github.com/en/billing/reference/billing-reports)
- [GitHub: dependency caching limits and the 7-day eviction policy (verified 2026-09-21)](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching)

---

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
