How to reduce GitHub Actions minutes

There are only two ways to use fewer CI minutes: make jobs shorter or run fewer of them. This page covers four changes that do one or the other, shows where each goes in a workflow file, and uses the calculator to show what each is worth on one example workload.

The example workload

A GitHub Team account with a private repository runs 40 jobs per working day, 6 minutes each, 22 working days a month, all on the Linux 2-core runner at $0.006 a minute (rate last checked on 2026-10-05; source: GitHub Docs, Actions runner pricing). That is 5,280 billable minutes against 3,000 included, and the calculator puts the overage at $13.68 a month. Every saving below is measured against that figure. The percentages are inputs chosen for the example, not measurements: what caching or cancelling saves in your repository is something you read off your own run history.

1. Cache dependencies so jobs get shorter

Downloading and building dependencies on every run is often the easiest time to remove. The actions/cache action restores a directory when a key matches, and the setup actions for common languages can do it for you.

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

GitHub's dependency caching reference explains keys and restore rules. It also says that cache entries not accessed in over 7 days are removed and that usage beyond the included cache storage is billed, so a cache is not free once it grows.

What it is worth here. If caching removes 30% of each job's time, the average job drops from 6 minutes to 4.2, which GitHub bills as 5. The month falls to 4,400 billable minutes, the overage to $8.40, and the saving is $5.28 a month or $63.36 a year.

Mind the rounding. If caching removes only 10%, the average job is 5.4 minutes and is still billed as 6. The calculator shows a saving of $0.00. Each job is rounded up to a whole minute, so a speed-up only lowers the bill when it carries jobs across a minute boundary.

2. Use path filters so unrelated changes do not start a run

A change to documentation does not need the full test suite. With paths or paths-ignore on the push and pull_request events, a workflow runs only when matching files change.

on:
  pull_request:
    paths:
      - 'src/**'
      - 'package-lock.json'

The workflow syntax reference lists the pattern rules. One caution: if a filtered workflow is a required status check, pull requests that skip it can be left waiting, so plan the filters together with your branch protection.

What it is worth here. If filters stop a quarter of the jobs from starting, 30 jobs a day run instead of 40. The month falls to 3,960 billable minutes, the overage to $5.76, and the saving is $7.92 a month or $95.04 a year.

3. Cancel runs that a newer push has made pointless

When someone pushes three commits in a row to a pull request, the first two runs are usually wasted. A concurrency group with cancel-in-progress stops the older run when a newer one starts in the same group.

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Grouping by branch reference keeps runs on different branches independent. Leave this off for workflows where every run must finish, such as deployments.

What it is worth here. If a fifth of the jobs are cancelled before they use meaningful time, the month falls to 4,224 billable minutes, the overage to $7.34, and the saving is $6.34 a month or $76.03 a year. A job cancelled part-way has still used the minutes it ran, so the real saving is somewhat lower than a model that removes those jobs entirely.

4. Set job timeouts so a hung job cannot run for hours

A job that hangs on a stuck test or a waiting prompt keeps its runner, and keeps being metered, until something stops it. timeout-minutes sets that limit per job.

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15

Pick a limit a little above the longest healthy run. A timeout does not make normal jobs cheaper, so the calculator has no input for it; what it does is cap the cost of the abnormal ones. On the example's runner, a job stopped at 15 minutes uses 15 minutes of the allowance or of billed time, where a job left to hang uses every minute until GitHub's own limit ends it. On a macOS runner at $0.062 a minute each of those minutes costs about ten times as much.

Combining them

Calculator output from prices last checked on 2026-10-05, for the example workload.
ChangeBillable minutes per monthOverage per monthSaving per month
None5,280$13.68$0.00
Caching removes 10% of job time5,280$13.68$0.00
Caching removes 30% of job time4,400$8.40$5.28
20% of jobs cancelled4,224$7.34$6.34
25% of jobs filtered out3,960$5.76$7.92
Caching 30% and 20% of jobs cancelled3,520$3.12$10.56

The combined row saves $10.56 a month, or $126.72 a year. If the caches behind it take 10 GB more than the included cache storage, that storage costs $0.70 a month at $0.07 per GB (price last checked on 2026-10-05; source: GitHub Docs, GitHub Actions billing), and the net saving is $9.86 a month or $118.32 a year.

When none of this lowers the bill

Fewer minutes only save money once you are past your plan's included minutes. Halve the example to 20 jobs a day and the month is 2,640 minutes, inside GitHub Team's 3,000. The overage is $0.00 before and after any of these changes. They still make feedback faster and leave more of the allowance free, but the invoice does not move. Public repositories on standard runners are in the same position, because those minutes are free.

The other lever is the runner itself. Moving a job from a dearer runner to a cheaper one changes the rate applied to every minute; the runner pricing page has the rates.

Try your own percentages. The free calculator shows your current bill. The full version has a what-if for caching and cancelled jobs that returns the minutes saved and the net saving after cache storage.

More guides