Self-Hosted Runners vs GitHub-Hosted After the Price Cut

2026-11-15来源 dev.toEnglish

Originally published on kuryzhev.cloud

Your team moved CI to a pool of self-hosted runners two years ago because the hosted minutes bill kept growing. Now a finance person forwards a GitHub announcement and asks whether the pool is still worth the on-call load. That question deserves a calculation, not a gut call, because the numbers behind self-hosted runners changed at the start of the year.

GitHub-hosted Actions runners have been up to 39% cheaper since 1 January 2026, and the planned fee for self-hosted runner usage was postponed. Both points are in the official changelog. The postponement is not a cancellation, so any plan that depends on "self-hosted is free on the GitHub side" has an expiry risk.

Short answer: default to GitHub-hosted runners, and keep or add self-hosted runners only for a specific reason. Good reasons are:

  • private network access
  • special hardware
  • compliance boundaries
  • sustained high-volume load where your measured cost per minute is clearly lower

If your only reason was "hosted minutes are expensive", re-run the numbers first.

When this choice matters

The decision is not worth much attention for a small repo with a few short workflows. It matters in four situations:

  • You run a large monthly volume of CI minutes, especially on larger runner sizes.
  • Jobs need to reach private resources such as an internal registry, a database, or a Kubernetes API that is not public.
  • You need GPUs, ARM boards, macOS fleets, or big-memory machines that the standard hosted sizes do not cover.
  • An auditor requires builds to execute inside a specific network or account boundary.

Before comparing anything, measure what you actually run. The command below lists recent runs and sums wall-clock minutes per workflow. It is only an approximation, because billing counts minutes per job and rounds each job up to the minute.

# Approximate minutes per workflow from the last 200 runs (needs gh CLI, logged in) gh run list --repo OWNER/REPO --limit 200 \ --json workflowName,startedAt,updatedAt \ --jq 'group_by(.workflowName) | map({workflow: .[0].workflowName, minutes: (map((.updatedAt|fromdate) - (.startedAt|fromdate)) | add / 60 | floor)}) | sort_by(-.minutes)' 

Then compare the result with the billing usage report in your organization settings. The GitHub Actions documentation describes the current billing views.

Option A: GitHub-hosted runners

Pros

  • No fleet to run. Images, patching, scaling, and cleanup are GitHub's job. Every job starts on a fresh VM, so there is no state leaking between runs.
  • The price dropped. The reduction is "up to" 39%, so the real saving depends on runner type and size. Check the current rate table instead of assuming the maximum.
  • Safer for untrusted code. Pull requests from forks are far less dangerous on throwaway VMs than on a machine inside your network. You still need careful workflow triggers and secret handling.
  • Elastic. A burst of parallel jobs on release day needs no capacity planning, up to your plan's concurrent-job limit.

Cons

  • Per-minute billing punishes slow jobs. A 25-minute build with no caching costs the same every time.
  • Reaching private networks needs extra plumbing: a VPN, an overlay network, or a paid private-networking feature. Verify the current options in the docs.
  • Hardware choices are limited to what GitHub offers. Larger runners include some GPU and ARM options, so check the current list before ruling them out.
  • Cold caches. Dependency and Docker layer caches have to be restored on every run.

The cheapest improvement here costs nothing. Cache dependencies, cancel superseded runs, and set job timeouts so a hung job does not burn minutes:

# .github/workflows/ci.yml name: ci on: [pull_request] concurrency: group: ci-${{ "{{" }} github.ref }} cancel-in-progress: true # stop paying for outdated pushes jobs: test: runs-on: ubuntu-latest timeout-minutes: 15 # a hung job must not bill for 6 hours steps: - uses: actions/checkout@v6 # verify the latest major tag before pinning - uses: actions/setup-node@v6 with: node-version: 26 # Node.js 26 LTS cache: npm - run: npm ci && npm test 

Option B: Self-hosted runners

Pros

  • Network position. Jobs run next to your private services, so no tunnels are needed for integration tests or deployments.
  • Hardware freedom. You can use GPUs, large-memory hosts, or specific CPU architectures.
  • Warm caches. A host-level Docker layer cache, a dependency mirror, or a shared cache volume can cut wall-clock time considerably. With ephemeral runners, the cache must live outside the runner's disposable environment to survive between jobs. The gain depends on your build.
  • Predictable cost at high, steady volume. You pay for machines whether or not jobs run, which wins only when utilization is high.

Cons

  • You own the fleet: OS patching (a current host base is Ubuntu 26.04 LTS), runner version updates, disk cleanup, and autoscaling.
  • Security blast radius. A compromised workflow on a long-lived runner can read secrets from earlier jobs and reach your network. Do not attach persistent runners to public repositories.
  • Idle cost. Capacity sized for peak hours sits unused overnight.
  • Policy risk. The postponed self-hosted fee may return. Check the changelog before committing to multi-year plans.

If you go self-hosted, register ephemeral runners that take one job and then deregister. The flags below are the documented runner options. The registration token comes from the REST API.

Requesting the token requires an org-admin credential. Run step 1 from a supervisor or controller, not on the runner host, so jobs cannot read that credential.

# 1. On the supervisor: get a short-lived registration token (org-level runners) TOKEN=$(gh api -X POST /orgs/ORG/actions/runners/registration-token --jq .token) # 2. On the runner host: configure an ephemeral runner in a group restricted to private repos ./config.sh --url https://github.com/ORG --token "$TOKEN" \ --name "build-$(hostname)-$RANDOM" \ --labels linux,x64,private-net \ --runnergroup private-ci \ --ephemeral --unattended --replace ./run.sh # takes exactly one job, then exits and deregisters # Restarting run.sh alone will not work: fetch a new token and re-run config.sh # (ideally on a fresh VM or container) before the next job. # 3. Verify registration and state gh api /orgs/ORG/actions/runners \ --jq '.runners[] | [.name, .status, .busy] | @tsv' # expected shape: build-host-1234 online false 

Point jobs at it with runs-on: [self-hosted, linux, private-net]. For Kubernetes-based autoscaling, start from the self-hosted runner documentation. Treat any controller choice as a separate evaluation.

Decision matrix

Situation Better fit Why
Small or medium volume, public internet only GitHub-hosted No ops cost; the price cut widens the gap
Open-source or public repo with fork PRs GitHub-hosted Ephemeral VMs limit damage from untrusted code
Jobs must reach private databases or clusters Self-hosted (ephemeral) Network position beats tunnels, but isolate by runner group
GPU, exotic architecture, huge RAM Self-hosted Hosted sizes may not exist for it
Steady, high, 24/7 load with measured high utilization Self-hosted, maybe Only if your cost per minute, including staff time, is lower
Bursty load with long idle periods GitHub-hosted Idle capacity makes self-hosted expensive
Regulated builds that must stay in one boundary Self-hosted Compliance outweighs cost

Use this formula for the cost column and fill it with your own numbers:

# Self-hosted monthly cost per CI minute actually used (compute + storage + network + licenses + engineer_hours_per_month * loaded_hourly_rate) / billable_job_minutes_per_month # Compare against the current hosted per-minute rate for the same runner size. # Add a margin for the postponed self-hosted platform fee, if it returns. 

Evidence-based recommendation

The published evidence is limited to two facts from GitHub: hosted prices fell by up to 39%, and the self-hosted fee was deferred, not removed. Everything else depends on your workload, so treat vendor blog numbers and anyone's anecdote as inputs, not conclusions.

Here is a defensible order of operations:

  1. Run the measurement command above and pull the billing report. Find the three workflows that consume the most minutes.
  2. Move everything that does not need private access or special hardware to GitHub-hosted runners. Add caching, concurrency cancellation, and timeout-minutes.
  3. Keep self-hosted runners only for jobs with a named reason. Make them ephemeral, put them in a restricted runner group, and never attach them to public repositories.
  4. Recompute the formula quarterly, and again when GitHub revisits the self-hosted fee.

Watch out for: hidden staff time. A pool that "costs nothing" in cloud spend but takes a few engineer-days a month to patch and debug is often not cheaper.

Also watch for non-ephemeral runners that quietly accumulate disk usage, credentials, and stale tool versions. A typical failure is a full disk that fails every job on one host while the rest of the pool looks healthy.

When not to self-host: if nobody owns the fleet, if you build untrusted fork code, or if utilization is below your formula's break-even point. A hybrid setup is common and reasonable: hosted runners for most pipelines, plus a small self-hosted pool for the few jobs that need the network or hardware. For related CI/CD decisions, browse the rest of kuryzhev.cloud.

Related

原文:https://www.a8q9.cn/en/article/self-hosted-runners-vs-github-hosted-after-the-price-cut-be644b.html

相关文章