3.2.1. GitHub-Hosted vs. Self-Hosted Runners
💡 First Principle: The two runner families trade the same currency in opposite directions — hosted runners buy you clean-state guarantees and zero maintenance at the cost of control and per-minute billing; self-hosted runners buy you control, custom hardware, and network reach at the cost of owning the machine's security and lifecycle.
| Dimension | GitHub-hosted | Self-hosted |
|---|---|---|
| Provisioning | Fresh VM per job, auto-destroyed | Your machine, persists between jobs by default |
| OS options | Ubuntu, Windows, macOS images | Anything you install the runner app on (incl. ARM, GPU) |
| Preinstalled tools | Large curated toolset, updated weekly | Whatever you install |
| Billing | Per-minute (free tier for public repos) | Free from GitHub; you pay for infrastructure |
| Network reach | Public internet; static IPs on larger runners | Your VPC, internal registries, on-prem systems |
| Public repo safety | Safe by design | ⚠️ Not recommended — fork PR code execution |
| Scaling | Automatic, GitHub-managed concurrency limits | You scale (autoscaling, ARC on Kubernetes) |
Labels are how jobs find runners. Hosted runners use the standard labels (ubuntu-latest, ubuntu-24.04, windows-latest, macos-latest); self-hosted runners always carry the implicit self-hosted label plus automatic OS/architecture labels and any custom labels you assign. Multi-label matching is an AND, which is how you route work precisely:
runs-on: [self-hosted, linux, x64, gpu]
Two operational patterns are worth knowing by name. Ephemeral runners (--ephemeral at configuration) accept exactly one job and then deregister, restoring the clean-state guarantee that makes hosted runners trustworthy — the recommended posture for self-hosted fleets. Actions Runner Controller (ARC) is the Kubernetes operator that autoscales ephemeral runners as pods, the standard answer to "how do we run self-hosted at scale without idle machines." On the hosted side, larger runners offer more cores, more RAM, and optional static IP ranges, which is how you get hosted-runner convenience while still satisfying an IP allowlist on a downstream system.
⚠️ Exam Trap: Self-hosted runners are persistent by default — files, environment changes, and installed packages survive into the next job. That is both the performance advantage and the security hazard. If a scenario describes cross-job contamination or a "poisoned" build environment, the fix is ephemeral runners or containerized jobs, not a cleanup step.
Reflection Question: A team wants hosted-runner convenience but must reach a database that only accepts connections from three whitelisted IPs. Which two options does GitHub give them, and what does each cost them in exchange?