Back to Blog
Tutorial
Optimization
GitHub Actions

5 Ways to Reduce Your GitHub Actions Costs by 50%

CICosts Team
Engineering
December 19, 20255 min read


GitHub Actions is incredibly powerful—and can get expensive fast. After analyzing thousands of workflows across hundreds of repositories, we've identified the most impactful ways to reduce costs.

Here are five changes that can cut your GitHub Actions bill in half.

1. Right-Size Your Runners

Potential savings: 25-50%

GitHub offers runners from 2-core to 64-core machines. The pricing scales linearly, but performance doesn't always follow.

Many workflows default to larger runners "just in case," but most jobs don't need them:

| Runner | Cost/min | Best For |
|--------|----------|----------|
| 2-core | $0.008 | Linting, formatting, simple tests |
| 4-core | $0.016 | Unit tests, small builds |
| 8-core | $0.032 | Integration tests, medium builds |
| 16-core | $0.064 | Large builds, E2E tests |

How to optimize:
  • Measure actual CPU usage during your jobs (add htop or similar)
  • Start with smaller runners and scale up only if jobs time out
  • Use larger runners only for parallelizable work
  • Before: 8-core for everything

    runs-on: ubuntu-latest-8-core

    After: Right-size per job

    jobs: lint: runs-on: ubuntu-latest # 2-core is fine test: runs-on: ubuntu-latest-4-core # 4-core for tests build: runs-on: ubuntu-latest-8-core # 8-core only for build

    2. Cache Aggressively

    Potential savings: 20-40%

    Downloading dependencies on every run is slow and expensive. GitHub provides 10GB of cache per repository—use it.

    What to cache:
  • Package manager dependencies (npm, pip, cargo, etc.)
  • Build outputs (compiled assets, binaries)
  • Docker layers
  • Test fixtures
  • - name: Cache node modules
      uses: actions/cache@v4
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('/package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-node-
    
    Pro tip: Use actions/setup-* actions with built-in caching:
    - uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: 'npm'  # Automatic caching!
    

    3. Skip Unnecessary Runs

    Potential savings: 15-30%

    Not every push needs full CI. Smart filtering can dramatically reduce runs:

    Skip CI for documentation changes:
    on:
      push:
        paths-ignore:
    
  • '.md'
  • 'docs/'
  • '.github/*.md'
  • Skip CI for draft PRs:
    on:
      pull_request:
        types: [opened, synchronize, ready_for_review]
    

    jobs:
    test:
    if: github.event.pull_request.draft == false
    # ...

    Run expensive jobs only on main:
    jobs:
      unit-tests:
        runs-on: ubuntu-latest
        # Runs on all branches
    

    e2e-tests:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest-8-core
    # Only runs on main branch

    4. Optimize Matrix Builds

    Potential savings: 30-60%

    Matrix builds are powerful but can explode in cost. A 4x3x2 matrix is 24 parallel jobs—each one billed separately.

    Before:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [16, 18, 20]
        # 9 jobs per push!
    
    After:
    strategy:
      matrix:
        include:
    
  • os: ubuntu-latest
  • node: 20 # Primary target
  • os: ubuntu-latest
  • node: 18 # LTS
  • os: windows-latest
  • node: 20 # Windows check # 3 jobs instead of 9
    Tips:
  • Test the most common configurations, not all permutations
  • Run full matrix only on release branches
  • Use fail-fast: true to stop on first failure
  • 5. Parallelize Strategically

    Potential savings: 10-20%

    Parallelization can speed up CI—but each parallel job incurs overhead (checkout, setup, teardown). Sometimes running sequentially in one job is actually cheaper.

    When to parallelize:
  • Each parallel task takes 5+ minutes
  • Tasks are truly independent
  • You're optimizing for speed, not just cost
  • When to keep sequential:
  • Tasks share significant setup time
  • Total work is under 10 minutes
  • You want to minimize billable minutes
  • Example: Test splitting done right

    Instead of 10 parallel test jobs (10x overhead):

    jobs: test: runs-on: ubuntu-latest-4-core steps:
  • run: npm test -- --parallel --workers=4
  • # Use runner cores for parallelism, not separate jobs

    Bonus: Monitor and Alert

    The best optimization is continuous. Set up alerts to catch cost spikes before they become bills:

  • Daily threshold alerts - "Alert me if we spend >$50/day"

  • Anomaly detection - "Alert on 20%+ increase vs. average"

  • Workflow alerts - "Alert if deploy.yml exceeds $20/run"
  • This is exactly what CICosts does. Try it free →

    The Results

    Implementing these five changes typically yields:

    | Change | Typical Savings |
    |--------|-----------------|
    | Right-size runners | 25-50% |
    | Cache dependencies | 20-40% |
    | Skip unnecessary runs | 15-30% |
    | Optimize matrices | 30-60% |
    | Strategic parallelization | 10-20% |

    Combined, teams often see 40-60% reduction in GitHub Actions costs.

    Getting Started

  • Measure first - You can't optimize what you can't measure. Use CICosts or GitHub's billing CSV to understand your baseline.
  • Find the expensive workflows - Usually 2-3 workflows account for most of your spend.
  • Apply changes incrementally - Don't change everything at once. Verify each optimization works.
  • Monitor after changes - Make sure optimizations don't break your CI or slow it down unacceptably.
  • ---

    Want real-time visibility into your GitHub Actions costs? Get started with CICosts →

    Ready to optimize your CI/CD costs?

    Start tracking your GitHub Actions spending for free.

    Get Started Free