Tenki’s startup program is live: up to $50K in credits and grants.Apply
GitHub Actions

GitHub Actions Cost Optimization in 2026

Eddie Wang
Eddie Wangengineering

Share Article:

Your GitHub Actions bill last month was higher than you expected. You're not alone. Most teams run every job on default 2-core Linux runners, leave caches misconfigured, and never cancel redundant workflow runs. The result is a monthly bill that grows linearly with team size while delivering diminishing returns.

This guide covers the full optimization surface: per-minute pricing mechanics, runner right-sizing, cache hit rate improvements, concurrency controls, workflow auditing, and where third-party runners like Tenki change the economics. Each section includes specific numbers and YAML you can ship this week.

How GitHub Actions Pricing Works

GitHub bills Actions compute per minute, rounded up to the nearest whole minute for each job. Public repositories get free standard runner usage. Private repositories get a monthly allowance based on your plan: 2,000 minutes on Free, 3,000 on Pro and Team, and 50,000 on Enterprise Cloud.

Once you exceed your included minutes, every runner minute costs real money. And the cost varies dramatically by runner type.

Standard Runner Pricing

Here's what GitHub charges per minute for standard hosted runners:

  • Linux 2-core (x64): $0.006/min. This is the default ubuntu-latest runner and where most spend concentrates.
  • Linux 2-core (arm64): $0.005/min. 17% cheaper than x64 for the same core count.
  • Windows 2-core: $0.010/min. 67% more than Linux for the same workload.
  • macOS 3-core / 4-core: $0.062/min. Over 10x the Linux rate. This is where budgets get wrecked.

A team running 10,000 minutes/month on default Linux runners spends $60. The same workload on macOS? $620. That single choice is the biggest cost lever most teams ignore.

Larger Runner Pricing

GitHub's larger runners (available on Team and Enterprise plans) scale roughly linearly in price but not always linearly in performance. The key rates for Linux x64:

  • 4-core: $0.012/min
  • 8-core: $0.022/min
  • 16-core: $0.042/min
  • 32-core: $0.082/min
  • 64-core: $0.162/min

ARM64 runners are substantially cheaper at every tier. A 4-core arm64 runner costs $0.008/min versus $0.012/min for x64. That's a 33% discount for workloads that can run on ARM.

One detail teams miss: larger runners don't draw from your included minutes. Every minute is billed at the per-minute rate, even on public repositories.

Runner Right-Sizing: The 2-Core Trap

The default 2-core runner is the right choice for most jobs. But not all jobs. The mistake teams make is applying it universally without measuring.

A 4-core runner costs 2x per minute ($0.012 vs $0.006). But if your build is CPU-bound and parallelizable, the 4-core runner might finish in 55% of the time. You'd pay 2x the rate for 55% of the minutes, netting a 10% cost increase for a 45% faster build. Whether that trade-off is worth it depends on your feedback loop requirements.

For single-threaded jobs (most linting, simple test suites, deployment scripts), extra cores sit idle. You're paying double for nothing.

When Larger Runners Save Money

Bigger runners actually reduce total cost when the speedup exceeds the price multiplier. This happens with:

  • Parallel test suites that actually utilize multiple cores (Jest with workers, pytest-xdist, Go's parallel test flag)
  • Large compilation jobs (Rust, C++, large TypeScript projects) where the compiler can parallelize
  • Docker builds with multi-stage pipelines and parallelizable layers
  • Memory-bound jobs that OOM on 2-core runners (7 GB RAM) and need the 16 GB that comes with 4-core

The measurement approach is straightforward: run the same job on both runner sizes for a week, compare total billed minutes, and multiply by the per-minute rate. If the larger runner's total cost is lower, switch.

The macOS Premium

macOS runners cost $0.062/min for 3-4 cores, $0.077/min for 12-core M1, and $0.102/min for 5-core M2 Pro. These rates are 10-17x the equivalent Linux cost.

macOS is justified when you're building iOS/macOS apps that require Xcode, running UI tests on Apple platforms, or signing/notarizing macOS binaries. For everything else, cross-compile on Linux and run macOS-specific tests in a minimal job that takes two minutes, not twenty.

Cache Strategies That Compound

Caching is where small configuration changes produce outsized savings. Every minute your workflow spends downloading dependencies or rebuilding artifacts that haven't changed is money burned.

Cache Hit Rate Targets

Aim for 85%+ cache hit rates on dependency caches. Below 70%, something is wrong with your cache key strategy. You can check your repo's cache hit rate in the Actions tab under "Caches" or via the REST API.

The most common cache key mistake is keying only on the lockfile hash. That works when the lockfile changes rarely, but breaks when you have multiple OS/Node version matrix entries that produce different native binaries for the same lockfile. Add the runner OS and runtime version to your cache key.

What to Cache (and What Not To)

Not all caches deliver equal value. Here's how they rank by impact:

  1. Package manager caches (npm, yarn, pip, Go modules). Cache the package manager's global cache directory, not node_modules directly. The setup-node action handles this correctly when you set cache: 'npm'.
  2. Build artifact caches (compiled output, Rust target directories, Gradle build caches). These save the most time per hit but grow large quickly. Watch the 10 GB per-repo limit.
  3. Docker layer caches. Use Docker's built-in GitHub Actions cache backend (type=gha) with docker/build-push-action. This can cut Docker build times by 60-80% on subsequent runs.

Restore-Key Patterns

The restore-keys parameter in actions/cache is your fallback hierarchy. If the exact key misses, GitHub tries prefix matches. A good pattern for Node projects:

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

The restore key prefix means even when the lockfile changes, you'll restore the previous cache and only download the diff. Without restore-keys, every lockfile change triggers a full cold download.

Cache Size Limits

GitHub gives you 10 GB of cache storage per repository for free. Beyond that, it's $0.07/GB/month. Caches that haven't been accessed in 7 days are automatically evicted. If your repo hits the limit, older caches get purged first.

Monitor your cache usage. If you have 15 matrix combinations each producing a 600 MB cache, you're at 9 GB and evicting constantly. Consolidate matrix keys or reduce what you cache.

Concurrency Controls

A developer pushes to a branch, CI starts, they push again 30 seconds later with a fix, and now two workflow runs are burning minutes for the same branch. The first run is already stale. But it'll run to completion unless you tell it not to.

The cancel-in-progress Pattern

Add a concurrency group to your workflow and set cancel-in-progress: true:

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

This groups runs by workflow name and branch. When a new run starts for the same group, the previous run is cancelled immediately. On a team of 10 developers pushing to feature branches several times a day, this alone can cut 15-30% of total minutes.

One caveat: don't set cancel-in-progress: true on deployment workflows that target production. You want those to queue, not cancel.

Concurrency Group Design

The group key determines what gets cancelled. Common patterns:

  • ${{ github.workflow }}-${{ github.ref }} groups by workflow and branch. The most common and generally the right default.
  • ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} groups by PR number when available. Better for workflows triggered by both push and pull_request events.
  • deploy-${{ github.ref }} for deployment workflows where you want queuing, not cancellation.

The Workflow Audit Checklist

Before changing runner sizes or cache configs, audit what you're actually running. Pull up your workflow files and work through these checks.

Trigger Hygiene

  • Running the full test suite on every push to every branch, when it only needs to run on PRs and main
  • Running CI on documentation-only changes. Use path filters: paths-ignore: ['docs/**', '*.md']
  • Running deployment checks on feature branches that will never deploy

Matrix Bloat

Matrix strategies multiply your job count. A matrix of 3 Node versions, 3 OS targets, and 2 package managers creates 18 jobs per workflow run. Do you actually support all 18 combinations in production?

Often the answer is no. Run the full matrix on the nightly or weekly schedule. For PR checks, run the primary combination and one or two edge cases.

Timeout Offenders

The default job timeout is 6 hours. A flaky integration test that hangs will burn 360 minutes before GitHub kills it. Set explicit timeouts on every job:

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

Set it to 2-3x your expected job duration. If tests normally take 5 minutes, a 15-minute timeout catches hangs early without false-positiving on slow runs.

Duplicate Work Across Workflows

Teams accumulate workflows over time. You'll often find two or three workflows that each run npm install and npm run build independently. Consolidate into a single workflow with multiple jobs that share artifacts, or use reusable workflows to avoid duplicate setup.

Where Tenki Runners Change the Math

All the optimizations above work within GitHub's pricing structure. But the per-minute rate itself is a variable you can change by switching runner providers.

Tenki Runners charges $0.002/core/minute for x64 Linux runners. Here's how that compares to GitHub for common configurations:

  • 2-core Linux: GitHub charges $0.006/min. Tenki charges $0.004/min (2 cores x $0.002). That's 33% less.
  • 4-core Linux: GitHub charges $0.012/min. Tenki charges $0.008/min. 33% less.
  • 8-core Linux: GitHub charges $0.022/min. Tenki charges $0.016/min. 27% less.
  • 16-core Linux: GitHub charges $0.042/min. Tenki charges $0.032/min. 24% less.

The per-minute savings aren't the whole story though. Tenki runs on bare-metal servers, which means faster I/O and no noisy-neighbor performance variance. Teams consistently report 30% faster build times on Tenki compared to GitHub-hosted runners for the same workflow. Faster builds mean fewer billed minutes, which compounds on top of the lower rate.

Migration is minimal. Tenki works with your existing GitHub Actions workflows. You change the runs-on label, and Tenki even opens an automated PR to handle the migration. No YAML rewrite, no new CI system to learn.

Tenki's Free Starter Plan

Tenki's free Starter plan includes $10 in monthly credits, up to 5 concurrent runner jobs, and runners up to 4 cores with 8 GB RAM. That's enough to trial on a real workload before committing. The Team plan ($200/month) includes $100 in credits, up to 50 concurrent jobs, and runners up to 64 cores.

For a team spending $500/month on GitHub Actions, moving Linux workloads to Tenki could bring that closer to $300/month from the rate difference alone, before accounting for speed improvements.

Governance and Spend Tracking

Cutting costs is one thing. Keeping them cut requires governance.

Spending Limits and Budgets

GitHub lets you set budgets on metered products and sends email alerts at 90% and 100% of your included usage. On Enterprise plans, you can set spending limits that block usage once exceeded.

Set a budget 20% above your expected monthly spend. This gives headroom for legitimate spikes (release weeks, large PRs) while catching runaway workflows before they burn through your quarterly allocation.

Metrics to Track

For cost center reporting, track these metrics monthly:

  • Total billed minutes by runner type. Shows where the money goes.
  • Minutes per workflow run (average and p95). Identifies regressions in build time.
  • Cache hit rate per repository. Below 80% signals a caching problem.
  • Cancelled run percentage. Should be 10-20% on active repos if concurrency controls are working. Zero means you aren't cancelling stale runs.
  • Cost per merged PR. Total Actions spend divided by PRs merged. This is your north star metric for tracking unit economics over time.

Alerting on Spend Spikes

Beyond GitHub's built-in alerts, set up a scheduled workflow that queries the billing API daily and posts to Slack when the run rate exceeds your budget threshold:

name: Billing Alert
on:
  schedule:
    - cron: '0 9 * * 1-5'
jobs:
  check-spend:
    runs-on: ubuntu-latest
    steps:
      - name: Check Actions usage
        run: |
          USAGE=$(gh api /orgs/$ORG/settings/billing/actions \
            --jq '.total_minutes_used')
          echo "Minutes used this cycle: $USAGE"
          if [ "$USAGE" -gt 40000 ]; then
            curl -X POST "$SLACK_WEBHOOK" \
              -d '{"text": "Actions usage alert: '"$USAGE"' minutes used"}'
          fi

Run this on weekday mornings so you catch problems before they compound over a weekend.

The Optimization Playbook

If you're staring at an unexpected GitHub Actions bill, here's the order of operations:

  1. Add concurrency controls to every CI workflow. Three lines of YAML, immediate savings.
  2. Set job timeouts. Caps your worst-case spend per job.
  3. Audit triggers and matrices. Cut jobs that shouldn't be running.
  4. Fix caching. Get hit rates above 85%.
  5. Right-size runners. Measure before upgrading.
  6. Evaluate third-party runners. Once you've optimized everything else, the per-minute rate becomes the remaining lever. Tenki's free tier lets you benchmark on your actual workload.
  7. Set up governance. Budgets, alerts, and monthly reporting prevent backsliding.

Steps 1-3 are free and take an afternoon. Steps 4-5 require measurement over a week or two. Step 6 takes a few minutes to set up, then a billing cycle to compare. Step 7 is what keeps the savings permanent.

Tags

#cost-optimization#cicd-billing#runner-sizing

Recommended for you

What's next in your stack.