Skip to content
compiler.dev

Self-Hosted vs GitHub-Hosted Runners: Cost and Risk

You have three ways to run GitHub Actions jobs: GitHub-hosted runners, runners you operate yourself, and runners from a third-party provider. The right choice depends on your volume, your security needs and how much operations work you want. This guide gives a way to compare them with numbers.

The three options

GitHub-hosted. Zero upkeep, fresh VM per job, included minutes on private repos. Standard Linux 2-core costs $0.006 per minute, and larger runners (4 to 96 cores) are billed per minute without included minutes, per GitHub's pricing page.

Self-hosted. You run the runner application on your own machines: EC2, Kubernetes, bare metal or a Mac in a closet. You pay for the infrastructure and for the people keeping it healthy.

Third-party hosted. A vendor operates runners that you select with runs-on labels. You get more cores or cheaper minutes and no ops work, and you trust another party with your code and secrets.

Cost: a break-even formula

For self-hosted, monthly cost is roughly:

infra       = machines x hourly price x hours running
ops         = engineer hours per month x loaded hourly cost
self-hosted = infra + ops + storage + egress

Compare with GitHub's cost for the same minutes: minutes x rate. Example for a 4-vCPU runner: GitHub's 4-core Linux is $0.012 per minute, or $0.72 an hour of job time. A 4-vCPU EC2 instance such as c7a.xlarge is on the order of $0.20 an hour on demand in us-east-1 (check current prices with the EC2 cost calculator). If it is busy 50 percent of the time, you pay $0.40 for each busy hour: well under GitHub's $0.72. If it is busy 5 percent of the time, it costs $4 per busy hour, which is far more than GitHub's.

So utilization decides it. Autoscaling that shuts machines down when idle turns that problem into a boot-time problem. Count the ops line honestly: even four hours a month of an engineer costing $100 an hour adds $400, which is the saving on about 33,000 minutes of 4-core GitHub time.

Speed

Self-hosted machines can be faster than a hosted 2-core: you choose the CPU, local NVMe, and keep warm caches (Docker layers, package caches) on disk. Persistent disks make repeat builds quick, but they also cause the "works on the runner" bugs that come from dirty state. Ephemeral runners with a warm base image are a middle path.

Hosted VMs always start clean, which is slower for large dependency trees but reproducible.

Security

This is where self-hosted differs most.

  • Never attach self-hosted runners to public repositories. A pull request from a fork can run arbitrary code on your machine. GitHub's own docs warn against it.
  • Use ephemeral runners. Register with --ephemeral (or use just-in-time runner configs through the API) so each runner takes one job and is destroyed. A persistent runner keeps workspace files, caches and credentials between jobs, so one compromised job can poison the next.
  • Isolate the network. Runners usually sit inside your VPC and may reach internal services. Limit security groups, block the instance metadata service where possible (IMDSv2 with hop limit 1), and give each runner group only the permissions it needs.
  • Scope runner groups. Restrict which repositories can use a runner group. Use GitHub runner groups at the organization level.
  • Keep secrets short-lived. Use OIDC to AWS rather than keys stored on the machine.

For hosted runners, GitHub handles isolation. For third-party providers, read their isolation model: whether jobs run in single-use VMs or containers, and what they log.

Operations you take on

  • Patching the OS and runner software. Old runner versions stop receiving jobs; the runner updates itself, but the host does not.
  • Autoscaling. Kubernetes users run Actions Runner Controller; EC2 users run scale sets or a tool built for it. Capacity must follow the job queue or developers wait.
  • Image management: preinstalling Node, Java, Docker and the toolchain your jobs need, since hosted images include lots of tools that yours do not.
  • Cleanup of disk, Docker data and zombie processes on persistent runners.
  • Monitoring queue time, failures and cost.

If that list sounds like a part-time job, it is.

Spot instances

Self-hosting lets you use spot capacity, often a large discount against on-demand, at the cost of interruptions. See spot instances for CI runners.

When each option wins

SituationBest fit
Small team, under a few thousand minutes a monthGitHub-hosted
Need more cores for a few heavy jobsLarger runners or third-party
Steady high volume, ops capacity, special hardwareSelf-hosted, ephemeral
macOS builds with a high billDedicated or third-party Macs; see macOS runners for iOS CI
Jobs need private network accessSelf-hosted in your VPC
Strict compliance on where code runsSelf-hosted

Check GitHub's pricing for self-hosted

GitHub has discussed charging a platform fee for self-hosted runner minutes. Policies change; check the Actions billing docs before building a cost model that assumes self-hosted minutes are free.

FAQ

Is self-hosted always cheaper?

No. At low utilization or with significant ops time it often costs more than hosted runners. Compute your break-even with real minutes first.

Can I mix hosted and self-hosted?

Yes. Use runs-on per job: cheap jobs on hosted, heavy or private-network jobs on self-hosted.

What if my self-hosted pool is empty?

Jobs queue until a runner is available. Some setups use labels with a fallback to hosted runners; design that explicitly rather than hoping for it.

Do I need Kubernetes?

No. Many teams run ephemeral runners as EC2 instances from an autoscaling group. Kubernetes helps if you already operate it.

Where compiler.dev fits

compiler.dev sits between the options: it runs your existing workflow on faster machines, and falls back to your own runners automatically if capacity runs out. In our benchmarks it was faster on 15 of 19 stacks, by 1.1 to 3 times, against GitHub's 2-core runner. Use the comparison mode to measure your own jobs before deciding.

Made by compiler.dev. Free tools · Pricing