Skipbox

GitHub Actions too expensive? Find what's driving the bill, then cut it

A GitHub Actions bill is each job's minutes times its runner's per-minute rate, less the compute your plan includes, and GitHub "rounds the minutes and partial minutes each job uses up to the nearest whole minute" (GitHub Docs, read on November 9, 2026). macOS runners, at more than 10 times Linux, larger runners, re-runs, many short jobs and runs nobody reads are what make it grow. The detailed usage report's workflow_path and sku columns show which workflow the money went to, and concurrency, paths filters and timeout-minutes are the first cuts.

Skipbox publishes this guide and sells a runner. Every GitHub rate and rule here comes from GitHub's own pages, read on November 9, 2026 and linked under Sources.

Where the money goes

The rates that make up most bills, per minute on private repositories:

Three rules turn those rates into the bill:

  • Every job rounds up. A job that runs 1 minute 5 seconds bills 2 minutes. Ten jobs of 15 seconds bill 10 minutes.
  • Every run bills, including failed ones and re-runs. GitHub's example: a workflow that fails after 5 minutes and passes on a 10-minute re-run bills 15 minutes.
  • The plan includes some compute, in dollars. $12 a month on GitHub Free, $18 on Pro and Team, $300 on Enterprise Cloud. Past it, every minute bills. How many free minutes private repositories get turns those dollars into minutes on each runner.

Storage bills apart from minutes: artifacts and Packages over the plan's allowance (2 GB on Team) cost $0.25 per GB a month, and Actions cache over 10 GB per repository is billed too. The GitHub Actions billing guide has every rate and the full formula.

Find the expensive workflow

The detailed usage report

  1. Open Billing & Licensing (for an organization: Settings, then Billing & Licensing; you need to be an owner or a billing manager), then the Usage page.
  2. Click Get usage report, choose the detailed usage report, pick the period, and click Email me the report. The detailed report covers up to 31 days and is only in the web interface; the REST API returns the summarized one.
  3. Open the link in the email within 24 hours.
  4. In a spreadsheet, filter product to Actions, then make a pivot table: workflow_path as rows, sku as columns, the sum of gross_amount as values.

gross_amount is the cost before your included compute comes off, so it ranks workflows by what they really cost even in a month the included compute covered. The top two or three rows are usually most of the bill. Sort by quantity instead to see minutes.

One run's billable minutes

Open a run, and under Run details in the left sidebar click Usage. It shows each job's billable minutes, rounded up, for GitHub-hosted runners on private repositories. A run with 20 jobs of under a minute each bills 20 minutes or more, whatever its total running time.

Eight cuts, with the YAML

1. Cancel runs a newer push has replaced

When you push twice to a pull request, the first run's result no longer matters. concurrency with cancel-in-progress: true cancels it:

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

Put it at the top of the workflow. On main, where every commit's result may matter, leave it out or use a group that only pull requests share.

2. Skip workflows a change doesn't touch

A change to the docs doesn't need the backend's test suite. paths runs the workflow only when a matching file changed:

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

If the workflow is a required status check, GitHub's note applies: its checks "will remain in a 'Pending' state", and "A pull request that requires those checks to be successful will be blocked from merging." Check your branch protection rules before you add one.

3. Merge tiny jobs

Each job rounds up to a whole minute and repeats its own checkout and setup. Six jobs of 20 seconds bill 6 minutes; the same checks as steps of one job bill 2 or less:

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm run format:check

4. Stop hung jobs early

A job runs up to 360 minutes by default before GitHub cancels it. Set timeout-minutes to a little over its normal length:

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

A step can have its own timeout-minutes as well.

5. Use the smallest runner that works

A lint or labelling job rarely needs 2 cores. ubuntu-slim (1 core, 5 GB) costs $0.002 a minute (every runner's cores and memory). And a job on a larger runner should earn its rate: if the 16-core build takes 4 minutes and the 8-core one takes 6, the 8-core run bills $0.132 against $0.168.

jobs:
  lint:
    runs-on: ubuntu-slim

6. Cache dependencies

Downloading the same packages on every run costs minutes every time. The setup actions cache them for you:

steps:
  - uses: actions/checkout@v7
  - uses: actions/setup-node@v7
    with:
      node-version: 24
      cache: "npm"
  - run: npm ci

cache takes npm, yarn or pnpm. GitHub keeps 10 GB of cache per repository by default, removes entries not used for 7 days, and bills usage beyond 10 GB.

7. Keep macOS for the jobs that need it

Run linting, unit tests and anything platform-neutral on Linux, and keep macOS for the build or test that needs Apple's tools. If the macOS job doesn't need to run on every push, run it on a schedule or on release:

on:
  push:
    tags: ["v*"]
  schedule:
    - cron: "0 3 * * *"

8. Keep artifacts for days, not months

Artifacts and logs are kept 90 days by default. Most build outputs are useful for a few days:

- uses: actions/upload-artifact@v7
  with:
    name: build
    path: dist/
    retention-days: 5

An organization owner can also lower the default for every repository in the organization's Actions settings, between 1 and 400 days on private repositories. A shorter period applies to new artifacts only.

What each cut saves

Examples on GitHub, worked out at GitHub's rates read on November 9, 2026. Your own numbers come from the usage report.

The arithmetic: minutes × the runner's rate; the macOS row is 1,000 × ($0.062 − $0.006); the artifact row is 18 GB stored at 90 days against 1 GB at 5 days, with Team's 2 GB allowance taken off, at $0.25 per GB a month. Savings come off the bill only past your included compute; under it, they leave more of it for other jobs.

When cuts aren't enough

After the cuts, what's left is the work itself: builds and tests that run because you need them. On GitHub's runners that cost grows with every commit.

GitHub bills nothing for self-hosted runner minutes, and they don't draw on your included compute. A runner service registers its machines with your account as self-hosted runners, so a job you move there leaves your GitHub bill and goes on the service's.

Skipbox runs Linux x64 jobs from private repositories, each in a fresh Ubuntu 24.04 VM in Finland or Germany (EU), for $39, $79 or $149 a month with unlimited minutes and no usage charges. Your plans set how many jobs run at once and how large each can be; extra jobs wait their turn, and the bill doesn't change. You move a job by changing its runs-on line to skipbox, and move it back the same way.

Against ubuntu-latest (2 cores, $0.006 a minute) on GitHub Team, plan S is lower above about 9,500 job-minutes a month, and the job moved gets 4 vCPU. The flat-price runners guide explains the formula.

These stay on GitHub: macOS, Windows and Arm jobs, public repositories, and jobs larger than 16 vCPU.

The cost calculator does this page's "find" step for you. Signed in, it reads the last 30 days of your repositories' jobs and prices each Linux x64 job on GitHub and on the Skipbox plans that run it, by repository and workflow, and lists the jobs that stay where they are.

You can try it on your own jobs first: free for 7 days or 3,000 minutes, no card.

FAQ

Why is my GitHub Actions bill so high?

Usually one of five things: macOS jobs, which cost $0.062 a minute against $0.006 on ubuntu-latest; larger runners; many short jobs, each rounded up to a whole minute; runs that nobody needed, such as runs for a commit already replaced by a newer push; and re-runs, which bill again. The detailed usage report's workflow_path and sku columns show which workflow and which runner the money went to.

How do I see which workflow costs the most?

On the Billing & Licensing Usage page, click Get usage report, choose the detailed usage report (up to 31 days, web interface only), and open the emailed link within 24 hours. In a spreadsheet, filter product to Actions and make a pivot table with workflow_path as rows, sku as columns, and the sum of gross_amount as values.

Does cancelling a workflow run lower the bill?

Yes, for the minutes it no longer runs. You still pay for the minutes the job ran before it was cancelled, rounded up to the next whole minute. concurrency with cancel-in-progress: true cancels a run as soon as a newer one in the same group starts.

Do failed and re-run jobs cost money?

Yes. GitHub's own example: a workflow that fails after 5 minutes and then succeeds on a 10-minute re-run bills 15 minutes.

How much more does macOS cost on GitHub Actions?

The standard macOS runner costs $0.062 a minute, more than 10 times the $0.006 of ubuntu-latest on a private repository. Running only the jobs that need macOS there is usually the largest single cut.

Do self-hosted runners cost anything on GitHub Actions?

Not today. GitHub lists self-hosted runners as usage that remains free, and they don't reduce your included compute. A $0.002-a-minute charge announced on December 16, 2025 was postponed the next day.

Sources

All read on 2026-11-09.

Updated November 9, 2026

See your own GitHub Actions bill

The free cost calculator reads your repositories' last 30 days of runs and gives one fixed monthly amount for the same jobs.

Open the cost calculator

Unlimited GitHub Actions at a flat price

Change one runs-on: line. Any minutes, one price.

  • No card
  • 7-day free trial
  • Cancel anytime