Learn

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

  1. 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.
  2. Rank repositories by wasted dollars, not by total spend. The biggest bill is not always the biggest opportunity.
  3. Fix the ranked list with the standard levers: caching and cancel-in-progress for volume, path filters for monorepos, consolidation for rounding, timeout-minutes as cheap insurance everywhere.
  4. Attack retry tax at the source — the flaky suites behind it — starting with the workflows whose retries cost the most minutes.
  5. Re-measure. The same four numbers, the following month, tell you what actually moved and what merely felt productive.
  6. 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.
  7. 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.

schedule Fast setup · 14-day free trial

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.