Too much work per run
Find and fix CI cost hotspots
By Keith Mazanec, Founder, CostOps ยท Updated February 17, 2026
Your repository has 12 workflows. You spend two weeks optimizing the lint job. It saves 45 seconds per run. Meanwhile, one workflow accounts for 70% of your total CI spend, and nobody has looked at it. CI cost is almost always concentrated. A small number of workflows (often just one) drive the majority of your bill. Finding those hotspots and fixing them first yields outsized savings compared to spreading effort across every workflow.
Why GitHub Actions cost is concentrated in one workflow
GitHub Actions cost follows a power law. In most repositories, one workflow accounts for 55-75% of total CI spend. A 30% reduction in that single workflow saves more than a 10% reduction spread across all workflows combined. The highest-ROI approach to reducing GitHub Actions costs is to identify your most expensive workflow, break it down by job name, and apply targeted optimizations to the top-cost jobs.
Symptoms
How to tell if your CI spend is concentrated
Cost concentration is invisible unless you measure it. GitHub's billing page shows total minutes, not per-workflow breakdowns. Look for these patterns:
-
One workflow dominates the Actions tab. Open your repository's Actions tab and sort by workflow. If one workflow has 3-5x the run count of the next, it's almost certainly your cost leader. High frequency alone can make a moderate workflow the most expensive one.
-
The top workflow has expensive runners or long durations. A workflow that runs on macOS (~10x Linux rate) or uses larger runners (4-core at $0.012/min) compounds quickly. Even a 15-minute workflow at $0.012/min costs $3.60/day if it runs 20 times.
-
Optimization efforts feel scattered. Your team has tried caching, test splitting, and path filters across different workflows, but the bill hasn't meaningfully dropped. This usually means the optimizations missed the one workflow that actually matters.
Metrics
How much can you save by optimizing one workflow?
In most repositories, the top workflow accounts for 55-75% of total CI spend. A targeted 30% reduction in that workflow saves $234/month on a $1,200/month bill. Here's a typical distribution:
Before (unfocused optimization)
Optimizing 7 other workflows saves ~$50/mo total
After (targeted top workflow)
Save $234/mo · $2,808/year · 30% reduction in top workflow
A 30% reduction in your single most expensive workflow saves more than a 10% reduction spread across all 8 workflows combined. The savings scale with runner cost. On macOS runners at $0.062/min (~10x the Linux rate), that same top workflow costs $8,060/mo, and a 30% reduction saves $2,418/mo.
Fix 1
How to find which GitHub Actions workflow costs the most
GitHub's billing page shows total minutes consumed per repository, but not per workflow. To get a per-workflow cost breakdown, use the GitHub Actions workflow runs API. The /repos/{owner}/{repo}/actions/runs/{run_id}/timing endpoint returns billable minutes per runner OS (UBUNTU, WINDOWS, MACOS). Aggregate these by workflow name and multiply by the per-minute rate to get cost per workflow.
Here's a script that uses the GitHub CLI to pull workflow run data and calculate cost by workflow name. It queries the last 30 days and applies GitHub's standard per-minute rates.
#!/bin/bash # Calculate CI cost per workflow for the last 30 days # Requires: gh CLI, jq OWNER="your-org" REPO="your-repo" SINCE=$(date -v-30d +%Y-%m-%dT00:00:00Z) # Fetch workflow runs with timing data gh api --paginate \ "/repos/$OWNER/$REPO/actions/runs?created=>=$SINCE&status=completed" \ --jq '.workflow_runs[] | [.name, .run_started_at, .updated_at, .id] | @tsv' \ | while IFS=$'\t' read -r name started updated run_id; do # Get billable minutes per OS for each run gh api "/repos/$OWNER/$REPO/actions/runs/$run_id/timing" \ --jq "[\"$name\", (.billable.UBUNTU.total_ms // 0), (.billable.WINDOWS.total_ms // 0), (.billable.MACOS.total_ms // 0)] | @tsv" done | awk -F'\t' '{ # Accumulate minutes and apply per-minute rates linux[$1] += $2/60000 * 0.006 win[$1] += $3/60000 * 0.010 mac[$1] += $4/60000 * 0.062 } END { for (w in linux) { total = linux[w] + win[w] + mac[w] printf "%s\t$%.2f\n", w, total } }' | sort -t$'\t' -k2 -rn
One caveat: this script makes one API call per run, which can be slow for repositories with thousands of runs. GitHub rate-limits authenticated requests to 5,000/hour. For high-volume repositories, consider using GitHub's usage report CSV export (available under Settings → Billing → Actions) or a tool like CostOps that tracks this continuously.
Fix 2
How to identify the most expensive jobs in a GitHub Actions workflow
After identifying your costliest workflow, break it down by job name to find where the minutes go. A workflow with 6 jobs typically has one job that accounts for 40-50% of its total runtime. That single job is the highest-leverage optimization target.
Use the workflow jobs API to pull job-level timing. Each job has a started_at, completed_at, and runner_name, which tells you exactly how long it ran and on what runner type.
#!/bin/bash # Break down cost by job name for a specific workflow OWNER="your-org" REPO="your-repo" WORKFLOW="ci.yml" # your top-cost workflow file SINCE=$(date -v-30d +%Y-%m-%dT00:00:00Z) # Get recent completed run IDs for this workflow gh api --paginate \ "/repos/$OWNER/$REPO/actions/workflows/$WORKFLOW/runs?created=>=$SINCE&status=completed" \ --jq '.workflow_runs[].id' \ | while read -r run_id; do # Get jobs for each run gh api "/repos/$OWNER/$REPO/actions/runs/$run_id/jobs" \ --jq '.jobs[] | select(.conclusion == "success" or .conclusion == "failure") | [.name, ((.completed_at | fromdateiso8601) - (.started_at | fromdateiso8601))] | @tsv' done | awk -F'\t' '{ seconds[$1] += $2 count[$1]++ } END { for (j in seconds) { mins = seconds[j] / 60 printf "%s\t%.0f mins\t%d runs\t$%.2f\n", j, mins, count[j], mins * 0.006 } }' | sort -t$'\t' -k4 -rn
The output gives you a ranked list of jobs by cost. A typical result looks like:
| Job name | Total mins | Runs | Cost |
|---|---|---|---|
| test-integration | 4,200 | 420 | $25.20 |
| build | 2,520 | 420 | $15.12 |
| test-unit | 1,680 | 420 | $10.08 |
| lint | 840 | 420 | $5.04 |
| deploy-preview | 420 | 420 | $2.52 |
In this example, test-integration is 43% of the workflow's cost. That one job is the target. The lint job you spent a week optimizing? It's 9% of the workflow and an even smaller share of overall CI spend.
Fix 3
How to reduce reruns on your most expensive workflow
Reruns on your most expensive workflow cost the most because the per-rerun bill scales with the workflow's size. If 15% of runs for your top workflow are reruns, and that workflow costs $780/month, you're spending roughly $117/month on retries alone. Reruns are often more expensive than first attempts because caches may have been evicted and the full pipeline re-executes instead of just the failed jobs.
Start by checking if the workflow uses concurrency groups to cancel superseded runs. Then look at failure patterns: are most failures from flaky tests, infra timeouts, or genuine code issues?
name: CI on: pull_request: # Cancel stale runs on the same PR concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: test-integration: runs-on: ubuntu-latest # Set a timeout to cap runaway costs timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - run: npm ci - run: npm run test:integration
Adding timeout-minutes to your most expensive job is a simple safeguard. Without it, a hung test can burn minutes indefinitely. Set it to 1.5x the p90 duration (if the job normally finishes in 12 minutes, set 18). See our guide to reducing CI timeouts for more detail.
Fix 4
What optimization to apply to each CI job type
Match the optimization to the job type. Any optimization applied to your highest-cost job has a multiplier effect because that job runs on every trigger. The table below maps common GitHub Actions job types to their recommended optimizations.
| Job type | Optimization | Guide |
|---|---|---|
| test-* | Split by timing, run focused suite on PRs | Optimize CI tests |
| build | Cache outputs, build once and share artifacts | Build once, use everywhere |
| setup/install | Add dependency caching, consolidate jobs | Reduce setup overhead |
| deploy-* | Gate to main/tags, remove from PR runs | Separate deploy from CI |
| lint/typecheck | Run on changed files only, use incremental caches | Optimize lint/typecheck |
For test-heavy hotspot jobs specifically, here's an example of adding dependency caching and test splitting to reduce the most expensive job in your workflow:
test-integration: runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: shard: [1, 2, 3] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - run: npm ci # Split tests across shards by timing data - run: | npx jest --shard=${{ matrix.shard }}/3 \ --ci --forceExit
One caveat: splitting a 10-minute job into 3 shards adds 2 extra jobs of setup overhead (checkout, install, cache restore). If setup takes 2 minutes per shard, you go from 10 minutes to 3 × (2 + 3.3) = 16 total billable minutes. Only shard when the test time is large enough that splitting produces a net reduction in total minutes, not just wall-clock time. See our over-parallelized test suites guide for sizing guidance.
Reference
GitHub Actions per-minute runner pricing
GitHub Actions bills per minute of runner time, rounded up to the nearest minute per job. To calculate per-workflow cost, multiply billable minutes by the runner's per-minute rate. These are the standard hosted runner rates:
| Runner | Rate | Multiplier |
|---|---|---|
| Linux 2-core | $0.006/min | 1x |
| Windows 2-core | $0.010/min | ~1.7x |
| macOS 3-core (M1) | $0.062/min | ~10x |
| Linux 4-core | $0.012/min | 2x |
| Linux 8-core | $0.024/min | 4x |
Larger runners are available on GitHub Team and Enterprise plans. If your hotspot workflow uses ubuntu-latest-4core or similar, the cost is 2x per minute ($0.012 vs $0.006). Factor this into your calculation when comparing the cost of splitting jobs (which adds more runner-minutes) versus running them on a single larger runner.
Team / Enterprise Larger runners (4-core and above) require GitHub Team or Enterprise Cloud. Standard 2-core runners are available on all plans.
FAQ
Common questions about CI cost hotspots
Does GitHub show per-workflow cost on the billing page?
No. GitHub's billing page shows total minutes consumed per repository and per runner OS, but does not break down cost by workflow name. To get per-workflow data, use the Actions API timing endpoint or export the usage report CSV from Settings → Billing → Actions.
What percentage of CI cost is typically in one workflow?
In most repositories, the single most expensive workflow accounts for 55-75% of total CI spend. The top 3 workflows typically cover over 80% of the bill. This concentration is driven by a combination of run frequency, job count, and runner type.
Should I optimize all workflows equally?
No. Because CI cost is concentrated, optimizing your top workflow yields 5-10x the savings of optimizing lower-cost workflows. A 30% reduction in a workflow that accounts for 65% of spend saves $234/month on a $1,200 bill. The same effort spread across 7 smaller workflows saves roughly $50/month.
How does GitHub Actions round billable minutes?
GitHub rounds each job up to the nearest whole minute. A job that runs for 35 seconds is billed as 1 minute. This rounding matters most for workflows with many short jobs, where the rounding overhead can add 20-30% to the total bill.
Related guides
Speed Up CI Pipelines
Identify slow jobs and add caching, splitting, and parallelism to reduce total pipeline duration.
Reduce CI Failures
Find which workflows consume the most rerun minutes and fix flakiness at the source.
Optimize CI Tests on PRs
Test steps dominate PR runtime. Split, filter, and gate tests to cut minutes without losing coverage.
Build Once, Use Everywhere
Stop rebuilding in every job. Build once and pass artifacts downstream.