How to Reduce GitHub Actions Costs: Measure the Waste First
Cut GitHub Actions spend by measuring waste first — failure cost, retry tax, rounding, queue time — then apply caching, cancel-in-progress, right-sizing, and cheaper runners.
The fastest way to reduce GitHub Actions costs is to measure your waste before you optimize
anything: how many billable minutes go to failed runs, to retries of work
that already ran, to per-job minute rounding, and to pipelines
that run when nothing relevant changed. Once you know which repositories and workflows burn the
most wasted minutes, the standard levers — caching, cancel-in-progress concurrency, path
filters, timeout-minutes, runner right-sizing — can be applied in order of measured impact, and
cheaper third-party or self-hosted runners become the last step
instead of the first.
Most guides on cutting Actions spend run that order backwards. They open with a runner vendor's rate card, and the rate card is real — but switching runners scales down the unit price of your waste without removing any of it. A team that retries a sixth of its runs, lets a red default branch re-run a full suite for two days, and bills 24 rounded-up minutes for 16 minutes of matrix compute will keep doing all three on cheaper hardware. Measurement first also means the savings compound: every wasted minute you eliminate is saved at whatever per-minute rate you end up paying afterward.
Where the money goes
GitHub Actions bills per job, per minute, for private repositories running on GitHub-hosted runners. Three mechanics drive nearly every surprising bill:
- Per-job rounding. GitHub "rounds the minutes and partial minutes each job uses up to the nearest whole minute," per its runner pricing reference. A 61-second job bills as 2 minutes. A 10-second lint job bills as 1. Every job in a matrix rounds independently.
- Per-OS rates. The operating system a job runs on changes its price by an order of magnitude — see the table below and the macOS minute multiplier entry.
- Included minutes, then overage. Each plan includes a monthly allowance for standard runners — 2,000 minutes on Free, 3,000 on Pro and Team, and 50,000 on Enterprise Cloud, per GitHub's billing documentation as of 2026-07-28. Larger runners sit outside this system entirely: included minutes cannot be used for them, and they are not free even for public repositories.
Rates changed recently. GitHub cut hosted-runner prices by up to 39 percent (depending on machine type) effective January 1, 2026 and confirmed in effect on that date, per GitHub's December 2025 announcement — which makes any cost article or savings calculator quoting the older $0.008-per-minute Linux rate out of date. The standard-runner rates as of 2026-07-28:
| Standard runner (private repo) | Rate per minute | Relative to Linux x64 |
|---|---|---|
| Linux 2-core (x64) | $0.006 | 1.0x |
| Linux 2-core (arm64) | $0.005 | ~0.8x |
| Windows 2-core | $0.010 | ~1.7x |
| macOS 3- or 4-core | $0.062 | ~10x |
Public repositories on standard hosted runners are free, and self-hosted runners carry no per-minute charge from GitHub (more on both below). Artifact and cache storage bill separately — $0.25 and $0.07 per GB-month respectively, as of the same date. For the full mechanics — how the allowance depletes, what exactly a billable minute is, spending limits, and how to read your usage report — see GitHub Actions billing, explained.
Measure the waste first
Every number in this section is computable from data the GitHub API already exposes: run and job records with timestamps, conclusions, and attempt counts. You do not need access to your source code to see any of it, which also means a measurement tool does not need code access either — the entire analysis works from pipeline metadata.
Four buckets cover most recoverable waste:
| Waste bucket | What it is | Where it shows up in run data |
|---|---|---|
| Failure cost | Minutes spent on runs that end red | Run conclusions joined to job durations |
| Retry tax | Attempts beyond the first for the same run | run_attempt greater than 1 |
| Rounding and sprawl | Billed minutes far above wall-clock minutes | Per-job durations vs. rounded-up billing |
| Queue time | Waiting before jobs start executing | Job started_at minus run created_at |
Failure cost
A failed run consumes the same billable minutes as a green one — you pay full price for compute that told you "no." The measurement is straightforward: over a month, sum the job minutes of runs whose conclusion was failure, per repository, and multiply by your blended per-minute rate. When you also want a failure rate, divide by decisive runs — failures plus successes — so cancelled and skipped runs do not flatter the denominator.
In the loss model behind our calculator, compute is the smaller half of failure cost — the larger half is engineer time: waiting, re-triggering, context-switching, and debugging. Whether that holds for you depends on your blended hourly rate and how many engineers a red run blocks. The full model is covered in the cost of failed builds, and the calculator on our landing page turns your own run volume and failure rate into a monthly figure — there is no need to rebuild the math in a spreadsheet.
The highest-value slice to isolate is failures on the default branch. A red main blocks
everyone, and every push while it stays red re-runs a full suite that is likely to fail for the
same reason. Two days of that on a busy repository can outweigh a month of ordinary waste.
Retry tax
The retry tax is the cost of re-running work that already ran. GitHub
Actions records it precisely: any run with run_attempt greater than 1 was retried, and every
attempt beyond the first bought no new information about the code — it bought a second chance at
the same answer. Summing the duration of those extra attempts gives you an exact, not sampled,
retry bill.
Rank the result by wasted minutes, not by retry count. A flaky test that forces a re-run of a 25-minute integration suite matters far more than one that retries a 40-second job, even if the second fires ten times as often. Retry tax is usually Pareto-distributed — a handful of workflows carry most of it — so that ranking tends to produce a short fix list.
Rounding and matrix sprawl
Because each job rounds up independently, workflows split into many short jobs bill far more than they compute. A 24-way test matrix whose shards each finish in 40 seconds uses 16 minutes of wall-clock compute and bills 24 minutes — a 50 percent surcharge that appears nowhere in any dashboard unless you compute it. Add a separate 15-second lint job, a 20-second commit-message check, and a 30-second changelog gate on every push, and you are paying three full minutes for about one minute of work.
The measured signal is the gap between summed wall-clock job time and billed minutes. Where the gap is large, consolidate: steps inside a single job share one rounding boundary, so the three checks above bill as a single 65-second job — 2 minutes instead of 3, and 1 minute if you can get the combined runtime under 60 seconds. For matrices, fewer and slightly larger shards often bill less than many tiny ones — the right shard count is the one where per-shard runtime comfortably exceeds a minute.
Queue time
Queue time — the wait between a run being created and its jobs
starting — is not billed by GitHub, but it is paid by your team in idle engineer minutes. It is
also a sizing diagnostic. On GitHub's standard hosted runners, queues are usually short; if you
see persistent queueing, the bottleneck is typically a larger-runner pool with a concurrency
limit, a self-hosted fleet that is too small, or an org-level concurrency ceiling. Measure it per
job (started_at minus created_at), track the p90 rather than the average, and read it
together with duration — a pipeline that is slow end-to-end usually has more than one cause, and
the diagnosis is its own topic.
The standard levers
The levers below are ordered by typical return on an hour of engineering effort. Your measurement should reorder them: a repository drowning in retry tax gets nothing from a cache tweak, and a repository with a 45 percent cache miss rate gets nothing from concurrency rules.
Cache dependencies and build outputs
Restoring dependencies from a cache instead of the network routinely turns a multi-minute install
step into seconds. Use the built-in cache options of the setup-* actions (Node, Python, Java,
Go) where available, and actions/cache for everything else — compiler caches, Docker layers,
test fixtures. Two details matter for cost rather than speed: cache storage itself bills per
GB-month once you exceed the free allowance, so evict aggressively and avoid caching artifacts
that are cheap to rebuild; and a cache with a bad key that never hits is pure overhead — check
your hit rate in the Actions cache UI before assuming the lever is already pulled.
Cancel superseded runs
When a developer pushes three times in five minutes, the first two runs answer a question nobody is still asking. A concurrency group with cancellation stops paying for the remainder of them — minutes their jobs already executed are still billed, each partial job still rounds up, and cancellation itself takes up to GitHub's 5-minute cancellation timeout, so the saving is the compute that would have run after the cancel, not the whole run (cancelled vs. failed runs):
concurrency:
# key the group on the workflow name and branch ref
# (use the github.workflow and github.ref expression contexts)
group: workflow-and-branch
cancel-in-progress: true
Scope the group to workflow plus branch so each branch keeps exactly one live run, and leave cancellation off for deploy workflows, where a half-finished job is worse than a redundant one. One bookkeeping note: cancelled runs then appear in your history in volume, and they are not failures — keep the two apart in any metric you compute, or your failure rate will look worse after the optimization than before it (cancelled vs. failed runs).
Filter what triggers
The paths and paths-ignore filters on push and pull_request triggers stop a docs-only
change from running a full test matrix. In monorepos this is often the single largest lever — the
size of the win is simply the share of your pushes that touch no code the workflow builds, which
your own run history tells you directly. The main gotcha is required status checks — a check that
never runs because of a path filter can leave a pull request blocked, so pair path filters with a
lightweight pass-through job or adjust branch protection accordingly.
Set timeout-minutes everywhere
A hung job — a test waiting on a dead socket, a container that never becomes healthy — keeps
billing until GitHub's default job timeout kills it, and that default is 360 minutes (six hours)
as of 2026-07-28, per the
workflow syntax reference.
Set an explicit timeout-minutes on every job, at roughly twice its p95 duration:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
This lever costs minutes to apply and caps a failure mode that otherwise produces the single most expensive runs in your history. Your own duration percentiles tell you what the right ceiling is per job.
Right-size runners
Three moves, in increasing order of effort. First, migrate Linux x64 jobs to arm64 where your
toolchain supports it — at $0.005 versus $0.006 per minute (as of 2026-07-28), that is roughly 17
percent off for a one-line runs-on change, and many workloads also run faster per minute on the
arm64 machines. Second, audit anything running on Windows or macOS that does not strictly need
to: a macOS minute costs about ten Linux minutes, so cross-platform suites should run their
platform-specific slice — not the whole suite — on the expensive OS. Third, treat larger runners
with suspicion in both directions: paying for 16 cores only makes sense if the build actually
parallelizes across them, and the way to know is to compare measured duration before and after a
size change, not to assume.
Move heavy work off the critical path
Not every check needs to run on every push. Exhaustive compatibility matrices, mutation testing, long-running end-to-end suites, and dependency audits can run nightly on a schedule instead of per-commit, cutting their frequency by an order of magnitude while still catching regressions within a day. While you are in the schedule file, also prune it: scheduled workflows on repositories nobody ships anymore are a classic source of pure waste, and they never show up in anyone's code review.
Third-party runners
A healthy market of drop-in runner replacements — Blacksmith, Depot, WarpBuild, BuildJet,
Namespace, RunsOn, and others — offers faster machines at lower per-minute rates. Adoption is
deliberately trivial: change the runs-on label. As of 2026-07-28,
Blacksmith lists Linux x64 at $0.004 per minute with 3,000
free minutes per month, and Depot lists GitHub Actions runner
overage at $0.004 per minute (tracked per second, billed per minute) on plans starting at $20 per
month. Both also compete on speed — newer bare-metal or optimized hardware frequently finishes
the same job in fewer minutes, which compounds the rate saving.
Two honest qualifications. First, the January 2026 price cut changed the arithmetic under some of the category's marketing: as of 2026-07-28 Depot's homepage still says its runners cost "half the cost of GitHub-hosted runners," a ratio that matches GitHub's old $0.008 Linux rate rather than today's $0.006 — against which the same $0.004 rate card saves about a third. (Blacksmith's pricing page has already rebased, quoting 33 percent.) Still real money at volume — just check any savings math against current GitHub prices before presenting it to whoever approves the migration. Second, a runner vendor is inside your trust boundary in a way an analysis tool is not: every job, including the full source checkout and any secrets it receives, executes on the vendor's infrastructure. That is not an argument against the category — the established vendors publish serious security postures — but it belongs in the evaluation.
Sequencing is the point of this article: a cheaper runner applied to an unmeasured pipeline locks in the waste at a lower unit price, and the discount often mutes the pressure to fix it. Measure, remove the waste you can, and then price the residual steady-state volume across vendors. The two product categories are complementary rather than competing — one runs your pipelines, the other tells you what they cost and why they fail — and the detailed comparisons are here: TrimCI vs. Blacksmith and TrimCI vs. Depot.
Self-hosted runners
Self-hosted runners remove GitHub's per-minute charge entirely — as of 2026-07-28, self-hosted minutes are free on the GitHub side. The cost does not disappear; it moves. You now pay for the machines (or cloud instances), for someone's time to patch and scale them, and for the security work of running ephemeral, isolated runners rather than long-lived snowflakes that accumulate state between jobs. The sizing trade-off is unforgiving in both directions: overprovision and you pay for idle capacity around the clock, underprovision and your measured queue time converts directly into idle engineers.
One recent episode is worth knowing when the business case rests on "self-hosted is free." In December 2025, GitHub announced a $0.002-per-minute "cloud platform charge" on self-hosted runner usage in private repositories, scheduled for March 1, 2026 — and then, after customer feedback, postponed it to re-evaluate the approach. No such charge is in effect as of this writing, but a plan whose entire margin depends on that remaining true should be re-checked against current pricing at decision time.
Self-hosted tends to win at large, steady Linux volume with an existing platform team, and to lose at small scale, where the maintenance overhead exceeds the runner bill it replaces.
An order of operations
- Pull 30–60 days of run and job metadata and compute the four waste buckets — failure cost, retry tax, the rounding gap, and queue time — per repository.
- Rank repositories by wasted dollars, not by total spend. The biggest bill is not always the biggest opportunity.
- Fix the ranked list with the standard levers: caching and
cancel-in-progressfor volume, path filters for monorepos, consolidation for rounding,timeout-minutesas cheap insurance everywhere. - Attack retry tax at the source — the flaky suites behind it — starting with the workflows whose retries cost the most minutes.
- Re-measure. The same four numbers, the following month, tell you what actually moved and what merely felt productive.
- Then price the residual volume against arm64, third-party runner vendors, and self-hosted, using current GitHub rates rather than a vendor's comparison page.
- Keep the measurement running. CI cost regressions arrive silently — a new matrix dimension, a cache key broken by a refactor, one flaky test in a hot path.
TrimCI automates the measurement half of this list. It computes failure cost, retry tax (from
run_attempt metadata), queue time, and duration percentiles per repository from synced run
history — pipeline metadata only, with a GitHub App that requests read-only access to Actions and
metadata and never to your code. It works at run granularity, with job-level rows
collected for failed runs only (it does not
detect individual flaky tests), and syncs by polling rather than webhooks, so it is a measurement
and reporting layer, not a real-time alerting one. The Free plan covers two repositories; paid
plans are priced from $9 per active contributor per
month, with a 14-day trial — details on the pricing page.
See what your CI failures actually cost.
Connect GitHub Actions or GitLab CI with read-only access — never your source code — and get a ranked, dollar-costed fix list from your own pipeline data.