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:
| Runner | Per minute |
|---|---|
ubuntu-slim (Linux 1-core) | $0.002 |
ubuntu-latest (Linux 2-core) | $0.006 |
windows-latest (Windows 2-core) | $0.010 |
| macOS (3-core M1 or 4-core Intel) | $0.062 |
| Linux 4-core larger runner | $0.012 |
| Linux 8-core larger runner | $0.022 |
| Linux 16-core larger runner | $0.042 |
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
- Open Billing & Licensing (for an organization: Settings, then Billing & Licensing; you need to be an owner or a billing manager), then the Usage page.
- 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.
- Open the link in the email within 24 hours.
- In a spreadsheet, filter
productto Actions, then make a pivot table:workflow_pathas rows,skuas columns, the sum ofgross_amountas 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.
| Cut | Example | Minutes saved a month | Saved a month |
|---|---|---|---|
| Cancel replaced runs | 100 runs a month replaced halfway through a 12-minute workflow on ubuntu-latest | 600 | $3.60 |
paths filters | 50 docs-only pushes skip a 12-minute workflow on a Linux 8-core runner | 600 | $13.20 |
| Merge tiny jobs | 6 jobs of 20 seconds become one 2-minute job, 500 runs on ubuntu-latest | 2,000 | $12.00 |
| Timeouts | 2 hung jobs stop at 20 minutes instead of 360, on a Linux 16-core runner | 680 | $28.56 |
| Smaller runner | 2,000 minutes of lint moved from ubuntu-latest to ubuntu-slim | 0 (same minutes, lower rate) | $8.00 |
| Caching | 100 seconds less per run, 1,000 runs on a Linux 4-core runner | about 1,700 | about $20.00 |
| Less macOS | 1,000 macOS minutes moved to ubuntu-latest | 0 (same minutes, lower rate) | $56.00 |
| Artifact retention | 200 MB of artifacts a day kept 5 days instead of 90, on GitHub Team | – | $4.00 |
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.
| Skipbox plan | Its largest job | Lower than GitHub's runner of that size on GitHub Team above |
|---|---|---|
| S, $39 a month | 4 vCPU, 8 GB | about 4,800 job-minutes a month (4-core, $0.012 a minute) |
| M, $79 a month | 8 vCPU, 16 GB | about 4,500 job-minutes a month (8-core, $0.022 a minute) |
| L, $149 a month | 16 vCPU, 32 GB | about 4,000 job-minutes a month (16-core, $0.042 a minute) |
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.
- GitHub Docs, Actions runner pricing: per-minute rates, rounding up per job.
- GitHub Docs, GitHub Actions billing: included compute by plan, the re-run example, storage allowances and the $0.25 per GB rate, self-hosted runners free.
- GitHub Docs, Billing reports reference: the summarized and detailed reports,
workflow_path,gross_amount, the web-only detailed report. - GitHub Docs, Viewing your usage of metered products and licenses: the Usage page and the emailed report.
- GitHub Docs, Viewing job execution time: a run's Usage view and billable minutes.
- GitHub Docs, GitHub-hosted runners reference:
ubuntu-slimandubuntu-lateston private repositories. - GitHub Docs, Control the concurrency of workflows and jobs:
cancel-in-progress. - GitHub Docs, Workflow syntax:
pathsand the note on skipped required checks,timeout-minutesand its 360-minute default. - GitHub Docs, Dependency caching reference: the 10 GB limit, the 7-day removal, billing past 10 GB.
- GitHub, actions/setup-node: the
cacheinput. - GitHub, actions/upload-artifact:
retention-days. - GitHub Docs, Configuring the retention period for GitHub Actions artifacts and logs in your organization: the 90-day default and the 1 to 400 day range.
- GitHub Changelog, Update to GitHub Actions pricing: the postponed self-hosted runner charge.
Updated November 9, 2026