Our runners against GitHub-hosted, measured.
The same open-source projects, built cold with their own CI commands on a 2 vCPU runner from each of us, 4 times a month. GitHub’s own records time both, and a person reads every round before it appears here.
Per workload, a whole job — queue, machine start, dependencies and build — was between 1.3× and 2.2× faster than on GitHub-hosted, each workload compared only over the rounds both runners completed.
The same builds cost $0.012 here against $0.034 there, at $0.004 a minute against $0.006. Both are billed time only, each by its own provider's rounding rule; the queue is in neither.
The result, workload by workload
One row each, every figure the whole job end to end and every percentage against our row. The queue column is the one we do not win; the method below says why, and it stays on the table because your pull request waits through it either way.
| Provider | Machine | eslint | cargo | prometheus | actionlint | Queue | Swing | Overall | Per minute | Cost per build |
|---|---|---|---|---|---|---|---|---|---|---|
| runners.io everything below is measured against this row | 2 vCPU / 8 GB | 157s 140–482s | 255s 246–323s | 175s 157–559s | not run | 36s | ±256% | baseline | $0.004 | $0.012 |
| GitHub-hosted | 2 vCPU / 8 GB | 197s+25% 168–227s | 561s+120% 354–628s | 270s+54% 196–352s | not run | 27s | ±80% | 1.67× slower | $0.006 | $0.034+183% |
The workloads
Three public projects, each checked out at a pinned commit, its toolchain installed the way the project’s own CI installs it, its dependencies fetched fresh, and then its own CI command. Nothing here was written by us to be fast on our runners.
| Workload | Project, at the pinned commit | Toolchain | Dependencies | Build |
|---|---|---|---|---|
| eslint | eslint/eslint c27bc926e496 | actions/setup-node, node-version lts/* | npm install --no-audit --no-fund | node Makefile mocha |
| cargo | rust-lang/cargo e8cb624d5701 | dtolnay/rust-toolchain@stable | cargo fetch --locked | cargo build --release --locked |
| prometheus | prometheus/prometheus d15adb9ad7e5 | actions/setup-go, go-version-file: go.mod, cache: false | go mod download | go test ./tsdb/ -test.tsdb-isolation=false |
How it is measured
Reading the table
Every percentage is against our own row, and on all of these columns less is better — so a positive number is slower or dearer than us, and a negative one beats us. Read the two rows here as two measurements built the same way, not as a map of the market. Shorter bar is better: each is drawn against the slowest row in its own column, which is why a bar cannot be compared across columns — a cargo build takes minutes and an eslint run takes seconds. Colour grades the gap on a fixed scale: yellow is level with us, orange half behind, red twice the time or the price. Our own row is the mark everything is set against and is shown green; on any other row green would mean that provider beat us, and nothing earns it for merely keeping up.
The machine on each row
Machine is the shape each provider advertises, at the label the benchmark asked for. Both rows are the same shape. GitHub’s is the private-repository runner; a public repo gets 4 vCPU / 16 GB and that row would move.
What arrives is not always what is advertised. Probed inside every timed job by saturating every visible core, we deliver 1.9 of our 2 advertised cores and GitHub delivers 1.8 of its 2. Both are medians over the 44 approved rounds from 2026-09-07 to 2026-10-08 in which our guest was shown the 2 vCPU we sell. Rounds before 2026-09-07 18:45 UTC ran our guest with 4 vCPU rather than the 2 we sell, and were withdrawn from this comparison because that machine is no longer sold.
What is timed, and who times it
Times are end-to-end wall clock — from the workflow being created to the job finishing, including queue, provisioning, checkout, toolchain, dependencies and build. They are read from GitHub’s own API rather than reported by the runners, so no provider times itself, ourselves included. GitHub-hosted is not the reference row, but it is measured exactly the same way: if that is what you run today, read that row. The cost column is not end to end; it is worked out from billed time only, below.
Queue
Queue is how long a job waits before a machine picks it up — created to started. It is already inside the workload times to its left, not added to them, so do not count it twice. Nobody charges for it, ourselves included: our meter starts when your code starts, so queue never reaches the cost column. It is here because your pull request is blocked for it either way, and a runner that is quick once it starts but slow to find you a machine is not a quick runner. The figure is a typical wait rather than a worst case: a job that queued three or more times its own provider’s median queue is dropped, and the longest wait it removed on this data was 95 seconds.
This is the column we lose. A job typically waited 36s for one of our machines, against 27s on GitHub-hosted. GitHub’s scheduler hands a job straight to its own runners. Ours has to hear about the job from GitHub, give it a fresh machine and register that machine as a runner before the job can be assigned to it — time the arrangement costs before any of your code runs, rather than a slow machine. It is already inside every end-to-end figure above, so the speed comparison pays for it in full.
How cost per build is worked out
Cost per build is worked out only from the time each provider bills: the job itself, from the moment a machine picks it up to the moment it finishes, as GitHub records it. Queue is in no row’s cost, ours included. Each provider’s minutes follow that provider’s own billing rule: we round each job up to the whole minute, with a one-minute minimum; GitHub rounds each job up to the whole minute. Each workload’s median job is billed under that rule at the provider’s published rate for the exact machine it was benchmarked on, and the workloads are averaged. Both rows use the pay-as-you-go rate — what a minute costs someone who signed up and ran a build, with no commitment and no prepaid discount.
Cold builds, and each row's configuration
Every provider’s configuration, in full. The benchmark is built to measure a cold build on every row: it never calls actions/cache, dependencies are fetched fresh each run, and toolchains are installed by each project’s own workflow. Anything that would carry state from one run into the next is turned off, so that a row measures a provider rather than that provider’s history. That is also the reason some of their features do nothing here, which we would rather say than let a cold run be mistaken for a full product comparison.
| Provider | Configuration |
|---|---|
| runners.io |
|
| GitHub-hosted |
|
Rounds, the window, and the one exclusion
Each figure is the median of the rounds that provider completed, taken from 44 rounds between 2026-09-07 and 2026-10-08 — the window reaches about 55 days back from the newest round, so older rounds stop counting — with the observed range beneath it and the worst swing in its own column. Providers miss rounds: a runner is never scheduled, a build falls over. So a comparison uses only the rounds both providers ran, recomputed rather than read off the two medians above — otherwise a provider that happened to miss the busy hours would be judged on its good ones. Rounds are never discarded for being incomplete; the round nobody finished is the one worth keeping. Not run means no provider completed that workload in the rounds a row rests on: the benchmark did not run it then, so it counts against nobody. The thinnest comparison here rests on 43 shared rounds. Rounds are taken 4 times a month, each at a different hour (04:17, 09:17, 14:17 and 20:17 UTC), so working hours are in the sample as well as the quiet hour: a benchmark run only at 04:00 measures the one time nobody’s CI is busy. Rounds before October 2026 were taken four times a day. Rounds that flatter us least still count — publishing only a favourable hour would be true and indefensible at once. One exclusion, applied to everybody. A job whose queue was at least three times that provider’s own median queue is dropped — that is GitHub’s scheduler, not the machine we are measuring. It is keyed to each provider’s own behaviour rather than to a round, so it cannot remove our bad day and keep a competitor’s. In this sample it removed 1 job: GitHub-hosted (1).
A person reads every round first
A round lands in our database unapproved, and nothing on this page, on the pricing page or anywhere else on the site reads it until somebody has looked at it and approved it. Approval means the round was read and is not obviously broken — a workload that half-ran, a runner wedged on a package mirror. It never means the round flatters us: the rounds where we look worst are approved on the same terms, and one approved by mistake can be revoked, at which point every figure here is recomputed without it.
Rounds run 4 times a month. If the newest approved round is more than 12 days old, a round either did not arrive or nobody has read it yet, and the top of this page says how old the comparison is rather than letting it pass for today’s. The speed and cost multiples are not stated at all until both runners share at least 3 approved rounds on every workload.
The rounds behind these figures
Every approved round in the current window, newest first. The repository the benchmark runs in is private — a public one would be given a larger GitHub runner than the one compared here — so its run pages cannot be opened from this site. The recipe below is how to check us instead.
| Round | Started | Completed by |
|---|---|---|
| 37810054147 | 2026-10-08 16:36 UTC | runners.io and GitHub-hosted |
| 37229224519 | 2026-10-04 19:41 UTC | runners.io and GitHub-hosted |
| 35709830736 | 2026-09-22 09:21 UTC | runners.io and GitHub-hosted |
| 35473016567 | 2026-09-19 22:20 UTC | runners.io and GitHub-hosted |
| 35457671139 | 2026-09-19 17:18 UTC | runners.io and GitHub-hosted |
| 35445034016 | 2026-09-19 13:12 UTC | runners.io and GitHub-hosted |
| 35433083125 | 2026-09-19 08:50 UTC | runners.io and GitHub-hosted |
| 35375909501 | 2026-09-18 17:42 UTC | runners.io and GitHub-hosted |
| 35257960150 | 2026-09-17 18:18 UTC | runners.io and GitHub-hosted |
| 35232754240 | 2026-09-17 14:19 UTC | runners.io and GitHub-hosted |
| 35078848755 | 2026-09-16 09:20 UTC | runners.io and GitHub-hosted |
| 35033220959 | 2026-09-15 22:54 UTC | runners.io and GitHub-hosted |
| 35006273693 | 2026-09-15 18:15 UTC | runners.io and GitHub-hosted |
| 34980987724 | 2026-09-15 14:21 UTC | runners.io and GitHub-hosted |
| 34952602659 | 2026-09-15 09:28 UTC | runners.io and GitHub-hosted |
| 34907520298 | 2026-09-14 23:09 UTC | runners.io and GitHub-hosted |
| 34886799890 | 2026-09-14 19:25 UTC | runners.io and GitHub-hosted |
| 34830590706 | 2026-09-14 09:56 UTC | runners.io and GitHub-hosted |
| 34786756827 | 2026-09-13 22:26 UTC | runners.io and GitHub-hosted |
| 34771565454 | 2026-09-13 17:26 UTC | runners.io and GitHub-hosted |
| 34760890737 | 2026-09-13 13:49 UTC | runners.io and GitHub-hosted |
| 34749774484 | 2026-09-13 09:35 UTC | runners.io and GitHub-hosted |
| 34722443108 | 2026-09-12 22:20 UTC | runners.io and GitHub-hosted |
| 34707607665 | 2026-09-12 17:14 UTC | runners.io and GitHub-hosted |
| 34694897318 | 2026-09-12 12:53 UTC | runners.io and GitHub-hosted |
| 34683869280 | 2026-09-12 08:40 UTC | runners.io and GitHub-hosted |
| 34654750657 | 2026-09-11 22:37 UTC | runners.io and GitHub-hosted |
| 34629521731 | 2026-09-11 17:46 UTC | runners.io and GitHub-hosted |
| 34605363716 | 2026-09-11 13:37 UTC | runners.io and GitHub-hosted |
| 34581712659 | 2026-09-11 08:57 UTC | runners.io and GitHub-hosted |
| 34538281748 | 2026-09-10 22:36 UTC | runners.io and GitHub-hosted |
| 34509919323 | 2026-09-10 17:43 UTC | runners.io and GitHub-hosted |
| 34483952465 | 2026-09-10 13:39 UTC | runners.io and GitHub-hosted |
| 34458122643 | 2026-09-10 08:59 UTC | runners.io and GitHub-hosted |
| 34412875581 | 2026-09-09 22:34 UTC | runners.io and GitHub-hosted |
| 34385410409 | 2026-09-09 17:51 UTC | runners.io and GitHub-hosted |
| 34359152632 | 2026-09-09 13:46 UTC | runners.io and GitHub-hosted |
| 34332193010 | 2026-09-09 09:00 UTC | runners.io and GitHub-hosted |
| 34287214404 | 2026-09-08 22:42 UTC | runners.io and GitHub-hosted |
| 34260089255 | 2026-09-08 17:57 UTC | runners.io and GitHub-hosted |
| 34233364977 | 2026-09-08 13:40 UTC | runners.io and GitHub-hosted |
| 34207068307 | 2026-09-08 08:54 UTC | runners.io and GitHub-hosted |
| 34168076418 | 2026-09-07 22:51 UTC | runners.io and GitHub-hosted |
| 34152889377 | 2026-09-07 18:45 UTC | runners.io and GitHub-hosted |
Re-run it yourself
- Use a private repository. GitHub gives a public one a 4 vCPU / 16 GB runner, not the 2 vCPU / 8 GB one compared here.
- Put both runners in one workflow run, one job each per workload, so every job starts from the same instant:
runs-on: ubuntu-latestandruns-on: runnersio-2vcpu-ubuntu-2404. (The benchmark asks for the older spelling of the same machine,ghwarp-2vcpu-ubuntu-2404.) - In each job, check out the project at the commit in the table above, install the toolchain as listed, fetch the dependencies and run the build command. Do not call
actions/cacheor carry anything from one run to the next. - Read the times from GitHub’s REST API rather than the job log: the run’s
created_atand each job’sstarted_atandcompleted_at. End to end is created to completed, queue is created to started, and billed time is started to completed. - Run it more than once, at different hours, and compare medians over the runs both runners completed. One run is one sample.