# RunsOn vs GitHub hosted runners, and the three bill lines

> RunsOn vs GitHub hosted runners: one metered invoice against a license plus your own AWS bill, with the lines the published comparison leaves out.

Source: https://latchkey.dev/learn/runners/runs-on-vs-github-hosted-runners  
Updated: 2026-09-21

RunsOn vs GitHub hosted runners is a choice between one invoice and three. GitHub sends a single metered bill that covers the machine, the cache, the artifacts and the people keeping all of it running; RunsOn sends a flat annual license and leaves AWS to bill you for the instances at spot, the S3 cache bucket and the transfer between them.

GitHub-hosted runners are the default and the baseline every other vendor prices against. On a private repository a standard Linux x64 runner is 2 vCPU with 8 GB of RAM at $0.006 a minute, Linux arm64 is $0.005, Windows is $0.010 and macOS is $0.062, with included minutes by plan and nothing to operate. On a public repository the standard runners are free, which is a fact no paid vendor can argue with and the first thing to check before reading any comparison.

RunsOn is a CloudFormation stack you deploy into your own AWS account. It launches one ephemeral EC2 instance per job, on spot with on-demand fallback, on any shape your quotas allow, and terminates it when the job ends. RunsOn charges a flat annual license keyed to how many jobs you run a month, and takes no cut of the compute: the instances, the volumes and the cache bucket all appear on your AWS invoice at AWS prices.

Every cell below was read from the vendor's own public pricing or documentation page on 21 September 2026. Where a vendor does not publish a number, the cell says "not published" instead of carrying a guess.

## Comparison

|  | RunsOn | GitHub-hosted | Latchkey |
| --- | --- | --- | --- |
| Price per minute, 2 vCPU Linux x64 | $0.0010 at spot for an m7i-flex.large in us-east-1, including a 30 GB gp3 volume at 400 MB/s, billed by AWS | $0.006 on a private repository, free on a public one | **$0.0025** (2 vCPU, 8 GB RAM, 100 GB disk) |
| Price per minute, 4 vCPU Linux x64 | $0.0017 at spot for an m7i-flex.xlarge on the same published table | $0.012 for the 4-core larger runner | **$0.005** (4 vCPU, 16 GB RAM, 100 GB disk) |
| Platform fee | A flat annual license per legal entity: $350 a year under 50,000 runners a month, then $1,050, $2,100 and $4,200. One license covers every org and AWS account you own | None. Your GitHub plan includes minutes rather than charging for the runner service | $5 to $49 a month by plan |
| Free tier | A 15-day trial into your own AWS account, and a free license for nonprofit, open-source, educational and personal non-commercial use | 2,000 minutes a month on Free and 3,000 on Pro and Team, 50,000 on Enterprise Cloud, plus unlimited free standard runners on public repositories | 30-day trial, then 2,000 included minutes a month on Developer, 4,000 on Launch, 6,000 on Scale |
| Billing granularity | AWS bills instances by the second. The license does not move with minutes; only two sustained months above a tier move you up one | Per minute, rounded up per job, charged to the repository owner | Per minute |
| Runner sizes and OS | Any EC2 shape your quotas allow, 1 to 96 vCPU on its published tables: Linux x64 and arm64, Windows, and GPU families. Its docs state macOS is not supported | Standard 2-core Linux and Windows, 3 or 4-core macOS, and larger runners to 96 vCPU on paid plans | 2, 4, 8 and 16 vCPU, Ubuntu 24.04 on x86_64 only. arm64, Windows and macOS are not offered |
| Cache: type, limit, persistence | Magic Cache backs `actions/cache` with an S3 bucket in your own VPC, with no transfer charge inside the VPC; sticky EBS disks and local NVMe for large working sets | 10 GB per repository included, configurable higher at $0.07/GB/month; entries unused for 7 days are removed, and the oldest are evicted first once the limit is hit | Size not published. `latchkey-dev/cache-action@v1` swaps one line for `actions/cache`, keeping `path`, `key` and `restore-keys`; entries are isolated per organization, versioned by OS, kept 14 days; a failed restore warns rather than failing the job |
| Docker build acceleration | Buildx against Magic Cache, an S3 backend, an ephemeral ECR registry, or sticky EBS disks when the layer set is large | `type=gha`, which writes layers into the same 10 GB per-repository store `actions/cache` uses | `latchkey-dev/docker-cache-action@v1`, layers held in a Latchkey-managed private registry per organization |
| Concurrency limits | Your EC2 service quotas and spot capacity rather than a plan tier. Fleet keeps a warm pool at high volume | By plan: a documented cap on concurrent jobs per account, separate for macOS | Not published; warm-pool capacity varies by plan |
| Where jobs run | Your AWS account, your VPC, your region, on instances that terminate after the job | Microsoft Azure, in GitHub's own subscription | Latchkey's own AWS estate on m6a-class hosts, one job per runner, private network, destroyed after the job |
| Self-healing or retries | Not offered | Not offered | Built in: transient failures are diagnosed and retried inside the run |
| Setup change required | Deploy a CloudFormation stack in AWS, then a `runs-on` label such as `runs-on=${{ github.run_id }}/runner=2cpu-linux-x64` | None. `runs-on: ubuntu-latest` is the default | A `runs-on` label change. GitHub organization accounts only |

## The three lines, and which two are not on the rate card

RunsOn is unusually direct about this and says it on its own pricing page: you pay three things and two of them go to AWS. The license is annual and flat, the EC2 compute is spot or on demand billed per second while a job runs, and the storage and transfer is the S3 cache bucket and the EBS volumes in your region.

A comparison that quotes only the license and the instance understates the total. The published $0.0010 a minute is an instance price with a 30 GB gp3 root volume folded in, not a job price: a runner in a private subnet reaches the internet through a NAT gateway charged by the hour and by the gigabyte, Magic Cache is S3 requests and S3 storage, and cross-zone traffic is billable. None of that is large per job, and all of it is real.

| What you pay for | RunsOn | GitHub-hosted |
| --- | --- | --- |
| The machine | Your EC2 bill, spot or on demand, per second | Included in the per-minute rate |
| The root disk | Your EBS bill. The published rates include a 30 GB gp3 volume at 400 MB/s | Included: 14 GB of SSD on a standard Linux runner |
| Cache storage | Your S3 bill, in your own bucket, with no transfer charge inside the VPC | 10 GB per repository included, then $0.07/GB/month |
| Artifact storage | Still GitHub: artifacts do not move with the runner | Pooled with Packages by plan, then $0.25/GB/month |
| Egress and NAT | Your AWS bill, by the hour and by the gigabyte | Included |
| The software | $350 to $4,200 a year by job count | Included |
| Operating it | Yours: quotas, AMIs, stack upgrades, spot capacity | GitHub's |

> The artifact row is the one people miss when they model a move. `actions/upload-artifact` talks to GitHub regardless of where the job ran, so artifact storage stays on the GitHub invoice at $0.25 per GB per month past your plan allowance, pooled with GitHub Packages. Only the cache moves.

## Speed

Not benchmarked yet; the runner benchmark report will replace this section. We have not run the same workload on these runners ourselves, and until we have there is no number here worth quoting.

The cheap way to get a real answer is to point one slow job at each candidate for a fortnight, because runner selection is per job and the Actions run log hands you the timings for free.

RunsOn does publish a like-for-like comparison, and it is more careful than most: a CPU score beside every instance row against the equivalent GitHub-hosted runner, with the instance named. Its x64 rows report scores above the GitHub-hosted number at every size, its Graviton rows are much closer, and two burstable arm rows report scores below it. That is a vendor benchmark on a vendor page, and it is at least specific enough to argue with.

The part you can check yourself for nothing is the cache. A cold `actions/cache` restore on a GitHub-hosted runner crosses a network you do not control; the same restore on RunsOn comes from an S3 bucket in the same VPC. If your jobs are dominated by dependency restore rather than by compute, that is where the time is, and [why is GitHub Actions slow](/learn/speed/why-is-github-actions-slow) has the measurements to tell the two apart.

## Cost: the same month, and where it stops being comparable

The workload every comparison here uses: 10,000 Linux minutes at 2 vCPU plus 2,000 macOS minutes on a private repository. RunsOn sells no macOS runner, so that half of its bill goes back to GitHub-hosted, which is also true of Latchkey.

|  | 10,000 Linux minutes at 2 vCPU | 2,000 macOS minutes | Monthly total |
| --- | --- | --- | --- |
| RunsOn on Starter | $10 of spot EC2 at its published $0.0010/min | Not offered: $124 on GitHub-hosted | $163, of which $29 is the license and $124 is still GitHub |
| GitHub-hosted | $60 at $0.006/min | $124 at $0.062/min | $184 |
| Latchkey | $25 at $0.0025/min | Not offered: $124 on GitHub-hosted | $149 plus $5 to $49 a month for the plan |

Read that table for its shape rather than its total. On Linux compute RunsOn is six times cheaper per minute at its own published spot rate, and that is the real result; on the month as a whole it is 11% cheaper, because macOS is two thirds of this bill and RunsOn does not sell macOS. A pipeline with no macOS in it looks entirely different: $10 plus $29 against $60, and the license has paid for itself by about 5,800 Linux minutes a month. Latchkey's own plan is $5 to $49 a month on top of that compute, and its 2,000 to 6,000 included minutes are not netted off, so the figure is gross on both counts.

On public repositories the comparison stops entirely. GitHub-hosted standard runners are free and unlimited there, which no license and no spot instance beats, and GitHub's own guidance is not to put self-hosted runners on a public repository at all, because a fork pull request would execute untrusted code on your network.

Then there is the line neither invoice prints. RunsOn hands you an AWS account to own, and GitHub documents that a runner more than 30 days behind the current release stops being queued jobs at all, so updating is a standing task. If that lands on somebody who already owns AWS accounts it is nearly free, and if it lands on whoever read the install guide it is the most expensive line on the page.

## Reliability

GitHub publishes a status page and no Actions SLA on the pages read here, and when it has a bad day everyone has it at once and nobody can act. RunsOn moves that risk to you, which is better and worse in specific ways rather than on balance. Spot capacity can run short, an EC2 quota you never raised will stop a widened matrix, and a drifted stack fails in a way GitHub support has never heard of. All three are yours to fix at three in the morning, which is the point and the cost.

One property only RunsOn has: the runner is inside your VPC, so a job that needs a private database, an internal registry or a service with no public endpoint reaches it without a tunnel. That is often the real reason teams move, and the compute saving is the argument they use to get it approved.

Neither side repairs a job that failed mechanically. A [Docker Hub pull rate limit](/learn/failures/docker-hub-pull-rate-limit-in-ci) at minute nine bills the minutes it burned and waits for a person on both, and on RunsOn it is hiding inside an EC2 invoice where nobody is looking for it.

## When to choose RunsOn

- Your repositories are private and your Linux volume is past roughly 5,800 minutes a month, which is where the Starter license stops costing more than the compute it saves.
- You need the runner inside your own network for a private database, an internal registry, a fixed egress address or a data residency commitment.
- You want instance shapes nobody sells as a runner size: GPU families, high-frequency instances, very large memory, or a specific generation your build is tuned for.
- You already operate AWS accounts, so quotas, AMIs and stack upgrades land on a rota that exists.
- Your project is non-commercial, where the license is free in exchange for a public acknowledgement.

## When to stay on GitHub-hosted

- Your repositories are public. Standard runners are free and unlimited there, and GitHub tells you not to put self-hosted runners on a public repository anyway.
- Your bill is mostly macOS. RunsOn does not sell it, so the move leaves the expensive two thirds of this page exactly where it was.
- Nobody owns an AWS account, or the person who would is already the bottleneck. The compute saving is smaller than one recurring interruption.
- You are inside your included minutes. Two thousand a month on Free and three thousand on Pro and Team is a real allowance, and paying a license to undercut something you are not being charged for is a loss.
- You want one invoice, one support channel and one place to look when a runner misbehaves.

## Switching: the exact diff

Deploy the stack first, then change one job and leave the rest alone for a fortnight. The RunsOn label is a query string rather than a name, and the run id in it is what binds the ephemeral runner to this run. Keep a GitHub-hosted job beside it on the same workflow so the comparison is same-commit rather than same-week.

```.github/workflows/ci.yml
jobs:
   test:
-    runs-on: ubuntu-latest
+    runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64
     steps:
       - uses: actions/checkout@v7
       - uses: actions/cache@v4
         with:
           path: ~/.npm
           key: npm-${{ hashFiles('package-lock.json') }}
       - run: npm ci && npm test

   test-control:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v7
       - run: npm ci && npm test
```

> The `actions/cache` step is unchanged on purpose. Magic Cache backs the same action with your own S3 bucket, so the workflow keeps its keys and its restore-keys and only the storage behind them moves.

## The verdict

For a private repository with real Linux volume and somebody who already owns the AWS account, RunsOn is the better buy and the margin is large: spot EC2 at its published rates is roughly a sixth of GitHub per Linux minute, the license stops growing with usage, and the runners sit inside the VPC where your private services already are. Say the rest of it out loud before you move. Two of the three bill lines are on an AWS invoice you now have to read, NAT and S3 and transfer are not in the published per-minute figure, macOS does not move at all, and the runner updates are now your standing task rather than nobody's. On a public repository GitHub-hosted is free and this comparison does not exist. Neither side repairs a job that failed for a reason unrelated to your code, and on RunsOn that failure is billed by AWS in a line nobody reads.

## FAQ

### Is RunsOn cheaper than GitHub-hosted runners?

On private-repository Linux compute, yes: RunsOn publishes $0.0010 a minute at spot for a 2 vCPU instance against GitHub's $0.006, and its license is flat. The Starter license pays for itself near 5,800 Linux minutes a month. On public repositories GitHub is free and nothing competes, and on macOS the comparison does not apply because RunsOn does not sell macOS runners.

### What is on the AWS bill when you run RunsOn?

RunsOn names three lines and two are AWS: the EC2 instances billed per second while a job runs, and the storage and transfer for the S3 cache bucket and the EBS volumes. Beyond those, a runner in a private subnet reaches the internet through a NAT gateway charged by the hour and by the gigabyte, and cross-zone traffic is billable. The published per-minute figures cover the instance and a 30 GB gp3 volume, not the rest.

### Do GitHub Actions artifacts move when the runner does?

No. `actions/upload-artifact` uploads to GitHub whichever machine the job ran on, so artifact storage stays on your GitHub invoice, pooled with GitHub Packages against your plan allowance and $0.25 per GB per month above it. Only the cache moves to your own bucket, at $0.07 per GB per month on GitHub against S3 pricing in your account.

### Can I use self-hosted runners on a public repository?

GitHub recommends against it, and the reason is specific rather than general: a pull request from a fork can run a workflow, so untrusted code would execute on a machine inside your network that later jobs also use. Keep public repositories on GitHub-hosted runners, where they are free anyway, and put self-hosted or vendor runners on private ones.

## References

- [RunsOn pricing: license tiers, the three bill lines and published EC2 per-minute costs (verified 2026-09-21)](https://runs-on.com/pricing/)
- [GitHub Actions billing: per-minute rates, storage rates and plan allowances (verified 2026-09-21)](https://docs.github.com/en/billing/concepts/product-billing/github-actions)
- [GitHub Docs: dependency caching limits, eviction and the 10 GB repository allowance (verified 2026-09-21)](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching)
- [GitHub: only use self-hosted runners with private repositories (verified 2026-09-21)](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access)
- [GitHub Docs: self-hosted runner automatic updates and the 30-day rule (verified 2026-09-21)](https://docs.github.com/en/actions/reference/runners/self-hosted-runners)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
