How to reduce GitHub Actions costs: the checklist
How do I reduce GitHub Actions costs?
Cut the minutes first, then the rate. Cancelling superseded runs, path-filtering untested changes, trimming matrix combinations and right-sizing runners all reduce billed minutes, which GitHub charges at $0.006 a minute for a standard Linux 2-core runner on private repositories. Storage is separate, at $0.25 per GB-month for artifacts and $0.07 for cache, with a 90-day default retention. Moving the remaining Linux jobs to a hosted runner at $0.003 a minute halves the rate. Rates read September 13, 2026.
Last checked September 13, 2026: GitHub's Actions billing documentation, runner pricing reference, concurrency, caching, matrix, schedule, limits and retention pages were read on that date, and every GitHub figure and quoted sentence on this page comes from that reading.
Which GitHub Actions cost cuts are worth doing first?
The ones that remove billed minutes, in the order of how many minutes they remove. At GitHub's published rate of $0.006 a minute for a standard Linux 2-core runner on a private repository, read September 13, 2026, the largest single line below is $95.04 a month from one scheduled workflow that runs more often than anyone reads it, and the smallest is $3.60 a month from cancelling superseded runs. Rate changes come last, because a cheaper rate on work you should not be running is still waste.
| Change | What it removes | Worked saving a month | Main risk |
|---|---|---|---|
| Lengthen an over-frequent schedule | 15,840 minutes | $95.04 | Slower detection of what the schedule watches |
| Trim two matrix combinations | 4,000 minutes | $24.00 | The dropped combination is the one that breaks |
| Cut artifact retention from 90 days to 7 | 83 GB-months of storage | $20.75 | An artifact deleted before anyone needs it |
| Path-filter a workflow | 1,800 minutes | $10.80 | A required check stays pending and blocks the merge |
| Cache the dependency install | 2,000 minutes | $12.00 | A cache that always misses, or costs more than it saves |
| Move a lint job to the 1-core runner | Nothing; the rate falls from $0.006 to $0.002 | $8.00 | 1 CPU and 5 GB of RAM is too small for the job |
| Cancel superseded runs | 600 minutes | $3.60 | A release cancelled halfway through |
| Right-size the runner | Varies with the job | Varies | A larger runner that does not finish proportionally faster |
| Move the remaining Linux jobs to a cheaper runner | Builds finish 1.8× faster, and the rate falls from $0.006 to $0.003 | $43.00 | Artifact storage keeps billing on GitHub |
Each figure is the arithmetic in the section that follows, on the volume that section states, at GitHub's published rates read September 13, 2026. Your volumes are different; the mechanism and the arithmetic are the transferable part, not the dollar. The per-minute rates and included minutes behind every line are on the GitHub Actions pricing page, and the GitHub Actions cost calculator applies them to your own monthly minutes.
How do I stop paying for superseded CI runs?
Cancel them with a concurrency group: a run that a newer commit has already superseded is still billed for every minute it executes, at $0.006 a minute for a standard Linux 2-core runner on a private repository, read from GitHub Actions billing on September 13, 2026.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
The mechanism. GitHub's workflow concurrency documentation, read September 13, 2026, states that "only a single job or workflow using the same concurrency group will run at a time", and that "to also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true". Queued work is handled the same way: "when a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default, any existing pending job or workflow in the same concurrency group will be canceled and the new queued job or workflow will take its place." Including github.workflow and github.ref in the group keeps unrelated workflows and unrelated branches out of each other's way; the group name is case insensitive.
The size of the saving. GitHub publishes no figure for how often a run is superseded, so the saving is your own supersede rate multiplied by the minutes each run would otherwise finish. Worked: a workflow of three 10-minute jobs bills 30 minutes a push. On 200 pushes a month with one push in five superseded, and cancellation landing on average halfway through, that is 40 cancellations saving 15 minutes each, 600 minutes, $3.60 a month at $0.006 a minute. The saving scales with the number of matrix jobs, because each cancelled job is a whole job's minutes.
The risk. Cancellation on a deployment workflow. GitHub's own page names deployments as a concurrency use case, but that is the case for serialising them, not for cancelling them: interrupting a release halfway through creates work rather than saving it. Put deploy jobs in their own concurrency group with cancel-in-progress: false, or leave them out. Two mechanical constraints from the same page: "the combination of queue: max and cancel-in-progress: true is not allowed and will result in a workflow validation error", and runs in a group are released "in first-in-first-out (FIFO) order according to the time each one started waiting on the concurrency group, not the time each workflow was dispatched".
Can path filters cut GitHub Actions minutes?
Yes, in proportion to the share of pushes that touch nothing the workflow tests: a workflow that does not trigger bills nothing, because GitHub bills the minutes each job executes, rounded up per job, read from GitHub Actions billing on September 13, 2026.
on:
pull_request:
paths:
- "app/**"
- "!app/**/*.md"
The mechanism. GitHub's workflow syntax reference, read September 13, 2026, states that "if at least one path matches a pattern in the paths filter, the workflow runs", and for the inverse that "when all the path names match patterns in paths-ignore, the workflow will not run. If any path names do not match patterns in paths-ignore, even if some path names match the patterns, the workflow will run." Order matters within one list: "a matching negative pattern (prefixed with !) after a positive match will exclude the path. A matching positive pattern after a negative match will include the path again." You cannot combine both lists on one event: "you cannot use paths and paths-ignore to filter the same event in a single workflow." Where a branch filter is also present, the events reference states that "if you use both the branches filter and the paths filter, the workflow will only run when both filters are satisfied."
The size of the saving. It is the share of pushes that skip, times the whole workflow. Worked: the same 30-minute-per-push workflow, 200 pushes a month, 30% of them touching only documentation and configuration outside the filter. That is 60 skipped runs, 1,800 minutes, $10.80 a month at $0.006 a minute. Unlike cancellation this saves the whole run rather than the tail of it, which is why it is worth more per event.
The risk, and it is the one that bites. A required status check that never reports. GitHub's troubleshooting page for required status checks, read September 13, 2026, states that when a required workflow is skipped by path filtering the "associated checks stay in a 'Pending' state and block merging", and its recommendation is blunt: "avoid requiring workflows that can be skipped." So decide first whether the check is required. If it is, keep the workflow triggering on every pull request and move the filter inside it — gate the expensive job on a change-detection step, and let a cheap job report the required check either way. If it is not required, the filter is safe. Test the rule against your branch protection settings before you rely on it, not after a release is stuck.
How much does caching save on GitHub Actions?
Exactly as much execution time as it removes, minus the restore and save steps, and the first 10 GB per repository costs nothing: GitHub's dependency caching reference, read September 13, 2026, states that "the total size of all caches in a repository is limited. By default, the limit is 10 GB per repository."
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-
The mechanism. A cache trades a download and build for a restore. The net saving per job is the install time without the cache minus the restore time and the save time, and because GitHub rounds each job's minutes up, a saving that does not cross a whole minute is not a saving at all. Key the cache on the lockfile so it changes when the dependencies change and not when the source does; restore-keys gives a near miss something to fall back on rather than a full rebuild.
The size of the saving. Worked: a dependency install of 3 minutes replaced by a 40-second restore saves about 2 billed minutes a job. Across 1,000 jobs a month that is 2,000 minutes, $12.00 at $0.006 a minute. The same caching reference bounds how fast you can use it: "you can create cache entries at a rate of up to 200 uploads per minute per repository, and download them at a rate of 1500 downloads per minute per repository."
The risks, in order of how often they happen. First, eviction: "GitHub will remove any cache entries that have not been accessed in over 7 days", so a workflow that runs weekly or less often will frequently find nothing and pay the restore attempt on top of the rebuild. Second, thrash: when a repository is over the limit "the cache eviction policy will create space by deleting the caches in order of last access date, from oldest to most recent", so a key that changes on every commit fills 10 GB with single-use entries and evicts the one that was working. Third, size: a cache big enough to take longer to restore than the install it replaces is a loss on every job, and it is worth timing both. Fourth, the bill above the limit, at $0.07 per GB-month. GitHub's own examples of a fully used allocation, read the same day, net the free 10 GB off first: 50 GB costs $2.80 a month, 200 GB costs $13.30, and 1,000 GB costs $69.30.
How do I trim a GitHub Actions matrix to spend fewer minutes?
Remove combinations, because every combination is its own billed job with its own rounding, and GitHub's usage limits, read September 13, 2026, cap a matrix at 256 jobs per workflow run on both GitHub-hosted and self-hosted runners.
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
version: ["3.11", "3.12", "3.13"]
exclude:
- os: ubuntu-latest
version: "3.11"
The mechanism. GitHub's matrix documentation, read September 13, 2026, states that "a job will run for each possible combination of the variables": three versions across two operating systems is six jobs, not one. Its worked exclude example takes a 12-job matrix down to 9 by removing two entries. Because rounding happens once per job rather than once per workflow, a matrix is also where partial minutes are most expensive: ten 30-second matrix jobs bill ten minutes, not five.
The size of the saving. It is linear in the combinations removed. Worked: a 10-minute job across six combinations bills 60 minutes a run; dropping two combinations bills 40. On 200 runs a month that is 4,000 minutes removed, $24.00 at $0.006 a minute. Dropping an operating system from the matrix removes a whole column of combinations at once, which is usually the larger cut.
The risk. The combination you drop is the one that would have caught the bug, and you find out in production rather than in the pull request. Drop by evidence: keep the versions you actually support and the oldest one you promise, and move the rest to a nightly or pre-release matrix that runs once rather than on every push. Two settings are easy to confuse here. fail-fast: true cancels the sibling jobs when one fails, which does save minutes, but it also hides whether the other combinations would have failed, so a red build often costs a re-run. max-parallel saves nothing at all: GitHub's page shows that it "will run a maximum of two jobs at a time, even if there are runners available to run all six jobs at once", which changes how long the workflow takes, not how many minutes it bills.
Which GitHub Actions runner size is cheapest for my job?
The one with the lowest completed-job cost, which is a runtime question rather than a rate question: at GitHub's published rates read September 13, 2026, a 4-core larger runner at $0.012 a minute has to finish a job in under half the minutes a standard Linux 2-core runner at $0.006 takes, or it costs more.
jobs:
test:
runs-on: ubuntu-latest # a standard 2-core Linux runner on a private repository
The break-even column below is $0.006 divided by the runner's rate: the share of the 2-core runtime the job must come in under for the larger runner to be cheaper. Rates read September 13, 2026 from GitHub's Actions runner pricing reference.
| Runner | Per minute (USD) | Must finish within this share of the 2-core runtime |
|---|---|---|
| Linux 1-core (x64) | $0.002 | 3.00× |
| Linux 2-core (arm64) | $0.005 | 1.20× |
| Linux 2-core (x64), standard | $0.006 | baseline |
| Linux 4-core (larger) | $0.012 | 0.50× |
| Linux 8-core (larger) | $0.022 | 0.27× |
| Linux 16-core (larger) | $0.042 | 0.14× |
| Linux 32-core (larger) | $0.082 | 0.07× |
The mechanism. Cores only help a job that can use them. A job that spends most of its wall-clock downloading packages, waiting on a database container or waiting on an external service runs at almost the same speed on four cores as on two, and bills twice as much. A job whose test runner shards across all available cores is the opposite case, and is where a larger runner pays.
The size of the saving, and the size of the loss. Worked both ways at the rates above: a 20-minute 2-core job costs $0.12. If the 4-core runner finishes it in 8 minutes, that is $0.096, a 20% saving. If it finishes in 12 minutes — a perfectly ordinary result for a partly serial job — that is $0.144, 20% more than doing nothing. The break-even is not "faster", it is "faster than the rate ratio".
The risks. Larger runners never draw on your included minutes: GitHub Actions billing, read September 13, 2026, states that they "are always charged for, even when used by public repositories or when you have quota available from your plan", so moving a job to one turns free minutes into billed ones even while your plan's quota sits unused. Larger runners are also offered only on GitHub Team and GitHub Enterprise Cloud. And measure with your real job, over several runs including a cache miss, rather than with a benchmark: the runner that wins on a synthetic build routinely loses on the one you actually ship. Note too, from GitHub's hosted-runner reference read the same day, that the standard runner is not the same machine on both sides of the fence — public repositories get a 4-core ubuntu-latest free and unlimited, while private repositories get the 2-core standard runner that the rates above describe.
Is the 1-core Linux runner cheaper for small jobs?
Yes, by two thirds: ubuntu-slim is 1 CPU with 5 GB of RAM and 14 GB of SSD, and bills $0.002 a minute against $0.006 for the standard Linux 2-core runner, from GitHub's hosted-runner reference and Actions billing read September 13, 2026.
jobs:
lint:
runs-on: ubuntu-slim
The mechanism. It is a rate change, not a minutes change. Nothing about the job gets shorter; the same minutes are billed at a third of the price, so the whole saving is available on any job whose runtime does not grow by more than 3× on one core. That is the same break-even the table above gives for the 1-core row.
The size of the saving. Worked: a 2-minute lint or formatting job run 1,000 times a month bills 2,000 minutes. At $0.006 that is $12.00; at $0.002 it is $4.00. The saving is $8.00 a month, 67%, and it applies to every job that is I/O-light and single-threaded — linters, formatters, YAML and licence checks, small script steps, jobs that exist only to report a required check.
The risk. 1 CPU and 5 GB is genuinely small. A job that pulls a large container image, compiles anything, or runs a test suite that expects parallel workers can take more than three times as long on one core, at which point the slim runner is more expensive than the standard one and slower as well. The 14 GB SSD is also the binding constraint for jobs with big checkouts or big build outputs. Move one job at a time and compare the billed minutes across several real runs before moving the next.
Are my scheduled workflows wasting GitHub Actions minutes?
Almost certainly, if any of them runs more often than anyone reads the result: GitHub's events reference, read September 13, 2026, states that "the shortest interval you can run scheduled workflows is once every 5 minutes", and a 5-minute schedule starts 8,640 runs in a 30-day month.
on:
schedule:
- cron: "17 6 * * 1" # weekly, Monday 06:17 UTC
The mechanism. A schedule bills on a clock rather than on a change, so it is the one source of minutes that grows with nothing at all. It keeps billing through a quiet week, a holiday, a frozen branch and a repository nobody has pushed to in a month.
The size of the saving. Worked: a 2-minute job on a 5-minute schedule is 8,640 runs and 17,280 billed minutes a month, $103.68 at $0.006 a minute. The same job hourly is 720 runs and 1,440 minutes, $8.64. Moving it from every 5 minutes to hourly removes 15,840 minutes, $95.04 a month — the largest single line in the table at the top of this page, and it is one line of cron. Every scheduled workflow is worth this arithmetic once.
The risks. A longer interval means later detection of whatever the schedule watches — an expired credential, a broken upstream dependency, a drifting deployment — so pick the interval from how long you can tolerate not knowing, not from habit. Two behaviours from the same reference are worth knowing before you tune one. Schedules are not punctual: "the schedule event can be delayed during periods of high loads of GitHub Actions workflow runs. High load times include the start of every hour", which is why the snippet above runs at 6:17 rather than 6:00. And the automatic cleanup only helps some repositories: "in a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days." A private repository has no such stop, and a scheduled workflow on an abandoned private repository bills indefinitely — that is the first place to look when a bill has no matching pushes.
How do I cut GitHub Actions artifact and cache storage costs?
Shorten retention: artifacts and logs are kept 90 days by default and bill $0.25 per GB-month above your plan's allowance, so the same uploads held 7 days accrue about 8% of the storage, from GitHub's Actions billing and repository settings documentation read September 13, 2026.
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
retention-days: 7
The mechanism. Storage is metered by how long a file exists, not by how many you upload. GitHub Actions billing, read September 13, 2026, states that "your monthly bill converts GB-Hours to GB-Months by dividing by the hours in the month (usually 720 hours for a 30-day month)", and that "at the end of the month, GitHub rounds your artifact storage to the nearest MB". Halving the days an artifact lives halves its contribution to the bill, whatever it contains.
The allowances the retention is measured against, and the rates above them, read September 13, 2026:
| Plan | Artifact and Packages storage included | Cache per repository |
|---|---|---|
| GitHub Free | 500 MB | 10 GB |
| GitHub Pro | 1 GB | 10 GB |
| GitHub Team | 2 GB | 10 GB |
| GitHub Enterprise Cloud | 50 GB | 10 GB |
| Rate above the allowance | $0.25 per GB-month | $0.07 per GB-month |
The size of the saving. Worked: a 1 GB build artifact uploaded once a day. At the default 90-day retention it reaches a steady state of about 90 GB stored; on GitHub Team, with its 2 GB allowance, that is 88 GB billed at $0.25, $22.00 a month. At retention-days: 7 the same uploads settle at about 7 GB, 5 GB billed, $1.25 a month. The saving is $20.75 a month for one line of workflow, and it scales directly with how large the artifact is.
The risk, and the limits. The obvious one is deleting a build before someone needs it — release assets, a binary a customer is waiting on, or the artifact from the failure you are about to investigate. Pick the retention from how far back you actually reach, and set it per upload rather than repository-wide where only some artifacts are short-lived. GitHub's repository settings documentation, read September 13, 2026, bounds what you can choose: "by default, the artifacts and log files generated by workflows are retained for 90 days before they are automatically deleted", and you can change it "to anywhere between 1 day or 90 days" on a public repository or "between 1 day or 400 days" on a private one. One thing that surprises people: when you customize the retention period, "it only applies to new artifacts and log files, and does not retroactively apply to existing objects", so shortening retention does not shrink this month's bill — the artifacts already uploaded keep their original expiry and have to be deleted by hand if you want them gone sooner.
Can I move my Linux jobs to a cheaper runner?
Yes, and it is the biggest single cut on this list, with no change to your tests. Sengi runs the same workflow at half the price per minute: Sengi's published rate is $0.003 a minute for 2 vCPU with 8 GB against GitHub's $0.006 for a standard Linux 2-core runner. Builds also finish up to 1.9× faster, so together it costs up to 74% less per build.
jobs:
test:
runs-on: sengi-standard-2-ubuntu-2404
Sengi's three sizes against their GitHub-hosted equivalents:
| Shape | Sengi per minute | Nearest GitHub runner | GitHub per minute |
|---|---|---|---|
| 2 vCPU, 8 GB | $0.003 | Linux 2-core, standard | $0.006 |
| 4 vCPU, 16 GB | $0.006 | Linux 4-core, larger | $0.012 |
| 8 vCPU, 32 GB | $0.012 | Linux 8-core, larger | $0.022 |
The mechanism. Only the runs-on line changes. Your workflows, secrets, logs and required checks stay in GitHub. Make the edit and merge, or give your coding agent the Sengi prompt and it opens the pull request for you. Per-minute billing, boot and teardown never billed, no subscription and no minimum.
The size of the saving. 12,000 Linux 2-core minutes a month on GitHub Team: GitHub bills 9,000 past the included minutes at $0.006, which is $54.00. On Sengi typical CI runs 1.8× faster, so the 12,000 minutes take 6,667; after Sengi's 3,000 free minutes, 3,667 at $0.003 is $11.00. You save $43.00 a month, 80%. And because it cuts the minutes and the rate, it shrinks whatever the earlier items on this list leave you with.
Up to 1.9× faster builds, so fewer minutes. A Sengi 2 vCPU runner runs on a 5 GHz AMD Ryzen 9 7950X3D, with up to 2.75× the multi-core CPU of GitHub’s 2 vCPU runner. A Node and Docker build ran 1.8× faster on the same 2 vCPU and cost $0.015 a run against $0.054 on GitHub, 72% less. On RunsOn’s open benchmark harness at 4 vCPU, a production Docker image built 1.9× faster at 74% less per build. Other real CI jobs like Vite and Mastodon cost 50–60% less per run on the same 2 vCPU: Vite’s build and test ran 1.3× faster and cost $0.024 a run against $0.060 on GitHub, and a Mastodon Docker image build cost $0.012 against $0.030.
Cache included. On Sengi the Actions cache is served from Sengi's own cache at no added charge, and Docker builds keep their layers with type=gha. Artifacts and logs stay on GitHub, so the artifact tips above still pay off. Weighing this against running machines yourself? The whole bill for that path is on the self-hosted runner cost page. To try Sengi: 3,000 free minutes a month, no card.
Cut your GitHub Actions bill by up to 74%.
Up to half GitHub’s price per minute, on 5 GHz bare metal. Builds finish up to 1.9× faster than on GitHub’s runners, so together you pay up to 74% less per build. Change one runs-on line, or give your coding agent one prompt and it opens the pull request for you.
3,000 free minutes a month, no card. See how switching works
