Introducing Tenki's code reviewer: deep, context-aware reviews that actually find bugs.Try it for Free
Runners
Jun 2026

Cut Your GitHub Actions Bill by 90% in 2026

Hayssem Vazquez-Elsayed
Hayssem Vazquez-Elsayedproduct

Share Article:

GitHub Actions is quietly becoming one of the largest line items in engineering budgets. A mid-sized team running 50,000 minutes per month on standard Linux 2-core runners is paying $300/month in compute alone — before storage, cache overages, or any macOS builds enter the picture. Scale that to a 4-core or 8-core runner for faster feedback, and you're looking at $600 to $1,100/month.

The good news: most of that spend is waste. Redundant dependency installs, unoptimized Docker builds, oversized runners sitting idle during I/O-bound tasks. This playbook covers seven layers of optimization, ordered from quick wins to architectural changes, with real configs you can copy into your workflows today.

The new math: GitHub Actions pricing in 2026

Before optimizing anything, you need to understand what you're actually paying for. GitHub's current pricing charges per-minute based on runner type and OS. Here's the breakdown that matters:

  • Linux 2-core (x64): $0.006/min — the "ubuntu-latest" default most teams use
  • Linux 4-core: $0.012/min — double the price, often for marginal speed gains
  • Linux 8-core: $0.022/min — the point where most teams start feeling pain
  • Windows 2-core: $0.010/min — 67% more expensive than Linux for the same core count
  • macOS 3-core: $0.062/min — 10x the cost of Linux. This is where bills explode.

On top of per-minute compute, you're paying for artifact and cache storage. GitHub gives you 10 GB of cache per repository for free, but artifacts and anything beyond that is billed at $0.07/GB-month. If your org has 50 repos each pushing 500 MB of build artifacts daily, storage costs add up fast.

Free plan users get 2,000 minutes/month. GitHub Team gets 3,000. Enterprise Cloud gets 50,000. After that, every minute is billed at the rates above. And here's the part most people miss: larger runners never draw from your free quota. If you're on a 4-core or larger runner, every minute is paid from dollar one.

Let's put together a realistic monthly invoice for a 15-person engineering team running a Node.js monorepo with Docker builds:

  • CI runs: ~25 PRs/day × 3 workflows each × 12 min avg = 900 min/day
  • Monthly minutes: ~19,800 on Linux 4-core ($0.012/min) = $237.60
  • macOS builds for iOS: ~3,000 min/month ($0.062/min) = $186
  • Artifact storage overage: ~$15
  • Total: ~$439/month

That's before any nightly builds, scheduled workflows, or deployment pipelines. Teams with heavier workloads routinely hit $1,000–$3,000/month. Let's fix that.

Layer 1: Dependency caching

The single highest-ROI change you can make. Most CI jobs spend 2–5 minutes downloading and installing dependencies from scratch on every run. With caching, that drops to seconds.

GitHub's built-in actions/cache action supports npm, yarn, pip, cargo, Go modules, and most other package managers. The setup is straightforward — here's a Node.js example:

- name: Cache node_modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

For Rust projects, caching the target/ directory and ~/.cargo/registry is even more impactful. Cargo builds are notoriously slow — a fresh Rust compile can take 10–15 minutes, but a cached incremental build often finishes in 2–3 minutes.

- name: Cache cargo registry and target
  uses: actions/cache@v4
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      target
    key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
    restore-keys: |
      ${{ runner.os }}-cargo-

A few things to watch for. Cache storage is capped at 10 GB per repo. If you're caching large artifacts across multiple branches, you'll hit that limit and caches start evicting. Use specific keys tied to your lockfile hash, and set restore-keys to fall back to partial matches so you don't lose all caching on a lockfile change.

Expected savings: 40–70% reduction in job time for dependency-heavy builds. On a $300/month bill, that's $120–$210 saved just from this one change.

Layer 2: Docker layer caching

If your pipeline builds Docker images, you're probably rebuilding every layer from scratch on each run. GitHub Actions doesn't persist the Docker build cache between jobs by default, so even unchanged base layers get rebuilt.

The fix is Docker BuildKit's cache backend, which integrates directly with GitHub Actions cache storage. Using docker/build-push-action with the GHA cache backend:

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build and push
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: myapp:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

The mode=max flag caches all layers, not just the final image layers. This matters for multi-stage builds where intermediate stages (like compiling Go binaries or installing system packages) are the slow part.

One gotcha: the GHA cache backend uses your repo's 10 GB cache quota. If you're also caching npm or cargo dependencies, you might run into eviction issues. For larger images, consider using a registry-based cache (type=registry) instead, which pushes cache layers to your container registry and doesn't count against the Actions cache limit.

Expected savings: 50–70% reduction in Docker build time. For a team building 3–4 images per pipeline, that's several minutes shaved off every CI run.

Layer 3: Parallelization done right

Parallelization reduces wall-clock time, but it doesn't automatically reduce cost. Running 4 parallel jobs for 5 minutes each costs the same as one job for 20 minutes. The savings come from a subtler place: each parallel job spends less time on setup, checkout, and teardown relative to its total runtime.

Where parallelization actually saves money:

  • Splitting test suites across matrix jobs lets you use smaller (cheaper) runners for each shard instead of one large runner for the full suite. Four 2-core runners at $0.006/min beat one 8-core at $0.022/min.
  • Separating independent workflows (lint, test, build) means a linting failure kills only its own job, not the entire 15-minute pipeline. Fail fast, pay less.
  • Path-filtered triggers are the underrated companion to parallelization. Use paths filters in your workflow triggers so docs-only PRs don't kick off the entire test matrix.

Here's a matrix strategy that splits a Jest test suite across 4 shards:

jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
      - run: npx jest --shard=${{ matrix.shard }}/4

When not to parallelize: if each shard spends more time on checkout and setup than actual testing, you're paying for overhead. Jobs under 2 minutes are usually better off running sequentially.

Layer 4: Smarter runner selection

Most workflows default to ubuntu-latest, which maps to a 2-core x64 runner at $0.006/min. That's reasonable for most tasks. But teams frequently upgrade to 4-core or 8-core runners for speed without measuring whether the extra cores actually help.

The truth is, many CI tasks are I/O-bound or single-threaded. Linting, type checking, and most npm install operations won't run any faster on 8 cores. You're paying 3.7x more ($0.022 vs $0.006) for zero speedup.

The right approach is to profile your jobs and match runners to actual needs:

  • Lint, format, type check: 2-core is plenty. These are single-threaded.
  • Unit tests: 2-core unless you're running parallel test workers.
  • Compilation (Rust, C++, Go): 4-core or 8-core, where parallelism actually helps.
  • Docker builds: 4-core is the sweet spot. Going above rarely helps unless you have heavy parallel compilation inside the build.

Also consider arm64 runners. GitHub's Linux arm64 2-core runner costs $0.005/min versus $0.006/min for x64 — a 17% discount. If your application targets arm64 anyway (which is increasingly common for container workloads on AWS Graviton or GCP Tau T2A), you get the cost savings and architecture-native testing in one move.

Layer 5: Self-hosted and alternative runners

Layers 1–4 are workflow-level optimizations. They're worth doing regardless. But the biggest single cost reduction comes from moving off GitHub-hosted runners entirely.

Self-hosted runners on your own hardware have zero per-minute charges from GitHub. You pay for the infrastructure, but you control the cost curve. The tradeoff is operational overhead: you're now managing machines, handling security patching, scaling capacity, and dealing with runner registration.

Managed runner services split the difference — you get cheaper per-minute rates without the ops burden. Tenki is one option worth looking at here. Their runners plug into your existing GitHub Actions setup with a single runs-on change and come in at $0.003/min for a 2-core Linux runner — half the $0.006/min GitHub charges.

Let's run the numbers with the same 15-person team from earlier:

  • 19,800 Linux minutes on Tenki 2-core ($0.003/min): $59.40 vs $237.60 on GitHub 4-core
  • Tenki's runners run on bare metal, which means builds tend to finish 30–60% faster (their published benchmarks show 40–67% faster on real-world repos like n8n and Citrea)
  • Faster builds mean fewer billed minutes, compounding the per-minute savings
  • Developer tier includes 1,700 free minutes/month, so lighter months might cost nothing at all

The migration is trivial — no workflow rewrites, no new YAML syntax. You change your runs-on label and everything else stays the same.

Layer 6: Workflow-level waste elimination

Beyond caching and runner selection, there's a category of savings that comes from just not running things you don't need to run.

Cancel redundant runs. When a developer pushes 3 commits in quick succession, you don't need 3 full CI runs. Use concurrency groups to auto-cancel in-progress runs when a new push arrives on the same branch:

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

This alone can cut your total minutes by 10–20% in active repos.

Timeout limits. GitHub's default job timeout is 6 hours. If a test hangs, you're paying for a runner doing nothing for up to 360 minutes. Set explicit timeouts:

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

Shallow clones. If your repo has years of git history, every checkout downloads all of it. For CI, you almost never need full history:

- uses: actions/checkout@v4
  with:
    fetch-depth: 1

On large monorepos, this can shave 30–60 seconds off every job. Across hundreds of daily runs, it adds up.

Artifact retention. By default, artifacts are retained for 90 days. Most teams never look at artifacts older than a week. Reduce retention to cut storage costs:

- uses: actions/upload-artifact@v4
  with:
    name: test-results
    path: coverage/
    retention-days: 7

How to audit your current spend

Before applying any of these optimizations, figure out where your money is actually going. GitHub provides a usage dashboard, but it's surprisingly easy to miss the biggest cost drivers.

  1. Go to your organization's Settings → Billing → Usage to see a breakdown by runner type and repository.
  2. Sort by total spend, not total minutes. A repo using macOS runners might have fewer minutes but higher cost than your busiest Linux repo.
  3. Look at the workflow run history for your top 3 repos. Identify which workflows run most frequently and which take the longest.
  4. Check for scheduled workflows (cron triggers) that might be running nightly or hourly without anyone checking the results.
  5. Look at cache utilization. The Actions cache usage API shows hit rates — if a cache key is missing more than 30% of the time, your key strategy needs work.

Set up budget alerts too. GitHub lets you configure notifications when usage hits 90% and 100% of your included quota. There's no reason to be surprised by a bill.

The quick-win checklist

Ranked by effort-to-savings ratio, starting with changes you can ship in under an hour:

  1. Add concurrency groups with cancel-in-progress to every PR workflow. Two lines of YAML, 10–20% savings.
  2. Set timeout-minutes on every job. Prevents runaway costs from hung processes.
  3. Cache dependencies with actions/cache. Biggest single time reduction for most workflows.
  4. Use fetch-depth: 1 for checkout. Saves 30–60 seconds per job on repos with long history.
  5. Drop artifact retention from 90 days to 7 days. Cuts storage costs by 90%.
  6. Enable Docker layer caching if you build images in CI. 50–70% faster Docker builds.
  7. Right-size your runners — downgrade I/O-bound jobs to 2-core. Profile before upgrading anything.
  8. Add path filters so docs changes don't trigger test suites.
  9. Audit scheduled workflows and kill any cron jobs nobody's monitoring.
  10. Try a managed runner alternative like Tenki for 50%+ savings with zero workflow changes.

Stack all of these together and the math gets dramatic. Caching cuts your minutes by 40–70%. Concurrency groups eliminate 10–20% of redundant runs. Right-sizing drops per-minute cost. And switching to a cheaper runner provider halves whatever's left. A team spending $1,000/month on GitHub Actions can realistically get below $100.

Start with the checklist. Pick the three easiest items for your codebase, ship them this week, and measure the impact in your next billing cycle. Most teams see results within days.

Tags

#ci-cd-architecture

Recommended for you

What's next in your stack.

GET TENKI

Smarter reviews. Faster builds. Start for Free in less than 2 min.