Latchkey vs self-hosted runners, with the ops bill counted
Latchkey vs self-hosted runners is not a per-minute comparison, because GitHub charges nothing for self-hosted Actions usage and your cloud provider bills for every hour an instance exists rather than for the minutes a job runs. Priced with the control plane, the idle capacity and a few hours of somebody's time on the same invoice, a small ARC fleet delivering 10,000 job minutes a month is the most expensive of the three options, not the cheapest.

Self-hosting means you run the runner software yourself, on EC2, on Kubernetes through actions-runner-controller, or on hardware in a rack. GitHub's billing documentation still states that Actions usage is free for self-hosted runners, and the $0.002 a minute platform charge announced for 1 March 2026 was postponed before it took effect, so today the GitHub side of that invoice really is zero. Everything else is yours: images, autoscaling, isolation, disk hygiene and the pager.
Latchkey sells the same shape of runner as a service: $0.0025 a minute for 2 vCPU and 8 GB with 100 GB of disk, billed per job minute, on a $5 to $49 monthly plan, with transient failures repaired on the runner. It has no fleet for you to size and no arm64, Windows or macOS, which is the trade at the heart of this page: self-hosting can run anything, and a managed runner runs one thing without asking you to operate it.
Every cell below was read from the vendor's own public pricing or documentation page on 20 September 2026. Where a vendor does not publish a number, the cell says "not published" instead of carrying a guess.
Self-hosted and Latchkey side by side (read 20 September 2026)
| Self-hosted, your own EC2 or ARC | Latchkey | |
|---|---|---|
| Price per minute, 2 vCPU Linux x64 | Nothing to GitHub. Your instance cost: RunsOn publishes $0.0016 a minute on-demand and $0.0007 at spot for a 2 vCPU m7i-flex.large in us-east-1, charged for every minute the instance exists | $0.0025 (2 vCPU, 8 GB RAM, 100 GB disk) |
| Price per minute, 4 vCPU Linux x64 | $0.0032 a minute on-demand and $0.0014 at spot for m7i-flex.xlarge, on the same published table | $0.005 (4 vCPU, 16 GB RAM, 100 GB disk) |
| Free tier | No GitHub charge at all, and self-hosted usage draws nothing from your included minutes | 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 from launch to termination, so you pay for queue time, warm pools and idle capacity | Per minute |
| Runner sizes and OS | Anything you can boot: x64, arm64, Windows, macOS on EC2 Mac, GPU instances, bare metal, any disk size | 2, 4, 8 and 16 vCPU, Ubuntu 24.04 on x86_64 only. arm64, Windows and macOS are not offered |
| Cache: type, limit, persistence | actions/cache still goes to GitHub and still stops at 10 GB per repository, unless you run a cache backend of your own | 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 | A warm layer cache on a disk that persists between jobs, which is the fastest option here and also how one job contaminates the next | latchkey-dev/docker-cache-action@v1, layers held in a Latchkey-managed private registry per organization |
| Concurrency limits | Whatever your autoscaler and your EC2 service quotas allow; the ceiling is an account limit, not a plan | Not published; warm-pool capacity varies by plan |
| Where jobs run | Your VPC, your account, your network placement, your compliance story | Latchkey's own AWS estate on m6a-class hosts, one job per runner, private network, destroyed after the job |
| Self-healing or retries | Whatever you write yourself, plus whatever your autoscaler does when a runner goes offline mid-job | Built in: transient failures are diagnosed and retried inside the run |
| Setup change required | A runner scale set or an AMI pipeline, an autoscaler, an image you keep patched, then runs-on: [self-hosted, linux, x64] | A runs-on label change. GitHub organization accounts only |
A worked month: 10,000 Linux job minutes on your own fleet
Take the Linux half of the workload every comparison here uses, 10,000 job minutes a month at 2 vCPU, and price three ways of delivering it. The self-hosted column is an actions-runner-controller fleet on EKS, which is the common shape, and the only input below that is not published by a vendor is the engineering time, which is stated as an assumption you should replace with your own.
| Line | Self-hosted on ARC | GitHub-hosted | Latchkey |
|---|---|---|---|
| Compute | $64: 40,000 instance-minutes at $0.0016, because 10,000 job minutes at 25% utilization needs four times the instance time | $60 at $0.006 a job minute | $25 at $0.0025 a job minute |
| Control plane | $73: EKS at $0.10 per cluster-hour for 730 hours | None | None |
| Plan or license | None | Included in your GitHub plan | $5 to $49 a month |
| Engineering time | $400: four hours a month at an assumed loaded $100 an hour | None | None |
| Monthly total | $537 | $60 | $30 to $74 |
The compute line is the one everybody models and the one that matters least. Even at spot prices, where RunsOn publishes $0.0007 a minute for the same instance, the fleet only falls from $537 to $501, because the control plane and the hours do not move. Turn the engineering assumption down to one hour a month and it is $237, which is still four times GitHub-hosted and eight times the Latchkey compute line. The point is not the exact figure, it is which term dominates. 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.
Utilization is the other term people get wrong. A fleet delivers job minutes only while a job is on it, so at 25% utilization you buy four instance-minutes per job minute. Run the simplest possible version instead, two always-on 2 vCPU instances and no Kubernetes: 1,460 instance-hours a month at $0.096 an hour is $140, and those 10,000 job minutes are 167 hours of work, so you paid $0.84 for each hour of job time against $0.36 on GitHub-hosted and $0.15 on Latchkey. The break-even arithmetic behind that, and what happens if the postponed platform charge ever arrives, is worked out in what a self-hosted GitHub runner really costs.
Where self-hosting genuinely wins
None of the above says self-hosting is a mistake. It says the cheap-looking cases are usually the expensive ones. Here is where the fleet is the right answer, and on any of these no per-minute rate competes.
- Hardware nobody rents you at a sane price: GPUs, very large memory, Apple silicon at scale, or a machine with a terabyte of local NVMe.
- Data that cannot leave your network. If the test suite talks to a database in your VPC, the runner has to be in that VPC and the argument is over.
- High, sustained utilization on committed spend. A fleet that is genuinely busy most of the day, on instances you have already paid for, is the one case where the compute line really is near zero.
- Jobs longer than the hosted limits, or builds that want a warm disk from the previous run, which no ephemeral runner can give you.
- A platform team that already operates a fleet for other reasons, so the marginal ops cost of CI runners is small rather than a new function.
The security line GitHub draws, and the one your disk draws
GitHub is explicit about one case: "We recommend that you only use self-hosted runners with private repositories. This is because forks of your public repository can potentially run dangerous code on your self-hosted runner machine by creating a pull request that executes the code in a workflow." That is not a preference, it is the reason ephemeral, single-use runners exist, and implementing them is part of the ops cost above rather than a default you inherit.
The quieter risk is state. A self-hosted runner that reuses a machine is fast because the disk is warm, and that same disk is how yesterday's Docker layers fill the volume and how one job leaves credentials or caches where the next one can read them. Disk-full failures are the most common symptom: no space left on device and freeing disk space on GitHub Actions runners measure what a cleanup actually reclaims, and on your own fleet that cleanup is a job you schedule rather than a fresh VM you are handed.
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 week, because runner selection is per job and the Actions run log gives you the timings for free.
Switching: the exact diff
Both directions are a label change in the workflow and a great deal of work outside it, but the work is asymmetric: adopting a managed runner is one line, and adopting a fleet is the scale set, the image, the autoscaler and the on-call rota that go with it. Leaving is symmetric, which is the honest reason to try the managed side first: if it does not work out, the revert is the same one line.
jobs:
build:
- runs-on: [self-hosted, linux, x64, ci-pool]
+ runs-on: latchkey-medium
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- run: make test
gpu-tests:
# stays self-hosted: no managed vendor on this page sells a GPU runner
runs-on: [self-hosted, linux, x64, gpu]The verdict
Self-hosted runners are free from GitHub and expensive from everyone else, and the expensive parts are the ones that do not appear on a rate card: idle instance-hours, an EKS control plane at $73 a month, and the engineering time that keeps images, autoscaling and disk hygiene working. On a month of 10,000 Linux job minutes those add up to roughly $537 against $60 on GitHub-hosted and $30 to $74 on Latchkey, and the ranking does not change until utilization is high and the ops hours are near zero. Where self-hosting wins it wins outright, because GPUs, VPC-local data and custom hardware are not purchasable at any per-minute rate, and a serious platform team can run a busy fleet cheaply. What a managed runner buys is the removal of a function, not just a discount, and the way to test that claim is to move one job and see whether anyone misses the fleet.
Frequently asked questions
When should you use self-hosted runners vs GitHub-hosted runners?
How much does it cost to run your own GitHub Actions runners?
What does actions-runner-controller cost to run?
minRunners, because every warm runner is an instance-hour you pay for whether a job arrives or not.Can I keep some jobs on self-hosted runners?
[self-hosted, linux, x64] while ordinary build and test jobs move to a managed label. The trap is leaving a fleet running for two jobs a day, because a fleet sized for a peak costs the same whether it serves two jobs or two hundred.Related guides
References
- GitHub Actions billing: Actions usage is free for self-hosted runners
- GitHub: only use self-hosted runners with private repositories
- RunsOn: published EC2 spot and on-demand per-minute costs by instance
- Amazon EKS pricing: $0.10 per cluster per hour
- Latchkey pricing: plans and per-minute runner rates
- GitHub Actions documentation