← FinOpsAid blog

Are self-hosted runners actually cheaper? How to model the real cost
Self-hosted runners look cheaper because the meter moves to a cloud bill nobody reconciles. How to model cost per busy-minute, and when the switch really pays.
The proposal arrives with a confident number attached. GitHub-hosted runners cost a published rate per minute; a VM costs a fraction of that per hour; therefore self-hosting saves a large percentage. The arithmetic is correct and the conclusion is frequently wrong, because the comparison is between a fully-loaded price and a partial one.
What makes this hard to catch is that self-hosting genuinely does remove a line from the GitHub invoice. The spend doesn't disappear — it moves to a cloud account owned by a different team, measured in a different unit, on a bill nobody reconciles against the workflows that caused it. It looks like a saving because the meter moved out of view.
The comparison everyone makes, and what it leaves out
A GitHub-hosted runner is billed per minute of use. You pay for the minutes a job runs and nothing else — no idle, no patching, no capacity planning. That per-minute rate looks expensive precisely because it's complete.
A self-hosted runner is billed for the machine, not the job. You're paying for every minute the instance exists, whether a workflow is running on it or not. So the honest comparison isn't rate against rate, it's rate against rate divided by utilisation, plus everything that isn't the instance.
Concretely: a fleet busy 25% of the time has an effective cost per busy-minute four times its sticker rate. Utilisation is the variable that decides this question, and it's the one the original proposal almost never includes.
The curve, not the sticker rate, is the comparison. The break-even point here assumes a particular maintenance burden; carry more of it and the crossing moves right, which is why "a VM is cheaper per hour" settles nothing. Illustrative rates.
What belongs in the denominator
Once you're modeling properly, a self-hosted minute has to absorb several things GitHub was previously handling.
Idle capacity. CI load is spiky and follows the working day. A fleet sized for peak sits mostly idle overnight and at weekends; a fleet sized for average queues badly at 11am. Autoscaling helps and doesn't eliminate it — scale-up latency means you keep warm capacity, and warm capacity bills.
Surrounding infrastructure. Instance storage, network egress, the control plane if you run an autoscaler.
Maintenance. Someone patches the image, rotates credentials, upgrades the runner agent, and debugs the job that fails only on your fleet. Recurring engineering time, and usually the term that flips the model.
Reliability. A hosted-runner outage is GitHub's problem. A self-hosted outage is your on-call, and it blocks every merge in the org until someone fixes it.
None of these are arguments against self-hosting. They're what has to be in the denominator before the comparison means anything.
The cases where it genuinely wins
Sustained high utilisation. If the fleet runs busy most of the working day the idle penalty collapses and the raw hourly rate starts to resemble the real rate. This is the main case, and it's why self-hosting tends to make sense at scale and not before.
Hardware the hosted tiers don't offer well. Large memory, GPUs, specific architectures, or builds that benefit enormously from a warm local cache. Here the saving often isn't cost per minute at all — it's minutes eliminated, because the build is faster.
Network position. Jobs pulling large artifacts from, or deploying into, a private network can avoid substantial egress and VPN complexity by running inside it.
Compliance requirements that make hosted runners a non-starter regardless of price.
Two of those aren't cost arguments at all. That's fine — "we self-host because we need GPUs" is a perfectly good reason. It just shouldn't be presented as a saving.
Modeled, and it has to say so
Self-hosted runner cost, expressed per workflow or per team, is modeled — always. No invoice line says "this workflow cost this much on your fleet."
What you have is a fleet cost from a cloud bill and an apportionment rule you chose: by job minutes, by runner-hours consumed, by some weighting of machine size. That rule is defensible if it's written down and arbitrary if it isn't, and the resulting figure belongs in the modeled bucket alongside the rest of your allocated costs. Presenting it as billed is exactly the confidently-wrong precision that costs you the room the first time someone checks.
Before you migrate, measure
- Find your current utilisation. If you can't state it you can't evaluate the proposal, and it's the dominant variable.
- Model cost per busy-minute, not per instance-hour. Fleet cost divided by minutes actually executing.
- Put maintenance in as recurring engineering time. Not a one-off setup cost.
- Compare against your real Actions bill, not the list rate. Included minutes and existing commitments change the baseline — and a handful of workflows drive most of your minutes, which may be worth optimizing before migrating anything.
- Decide the allocation rule up front. You'll need it the moment someone asks what a team's CI costs.
- Re-check utilisation quarterly. A fleet sized for last year's load is this year's idle bill.
The honest version of this decision is rarely "self-hosted is cheaper" or "hosted is cheaper." It's that a well-utilised fleet with a clear owner beats hosted at scale, an under-utilised one loses to hosted at any scale, and knowing which you have is a measurement rather than an opinion.
FinOpsAid keeps both sides visible: Runners inventories your self-hosted fleet with health and a modeled cost — labelled as modeled, with the rate basis shown — alongside hosted-runner inventory and utilisation, so the comparison runs against your actual usage rather than a list price.
Frequently asked questions
Are self-hosted GitHub runners cheaper than GitHub-hosted ones?
Only when they are well utilised. Self-hosted machines bill for existing, so idle capacity is pure loss, whereas GitHub-hosted runners bill only for the minutes consumed. The comparison people usually make omits that, which is why self-hosting looks cheaper on paper more often than it is in practice.
What belongs in a self-hosted runner cost model?
Compute and storage, plus the operational cost that never appears on any invoice: the engineering time spent patching, scaling, securing and debugging the fleet. That last term is routinely the larger one, and leaving it out is what makes the comparison misleading.
How do I compare hosted and self-hosted runner costs fairly?
Model cost per busy-minute rather than cost per month. A monthly figure rewards a fleet that sits idle; cost per busy-minute exposes it. Measure your actual utilization before migrating, because that single number decides which side of the comparison wins.
Want your real runner utilisation before the migration meeting? Connect your GitHub org — read-only and free during beta — or explore the demo dashboard first.