GitHub Actions are slow and expensive, what are the alternatives?
GitHub's default runners are 2 slow cores at a premium price. Here are the real alternatives (artisanal, productized, and SaaS self-hosted runners) and why I built RunsOn on ephemeral EC2.
GitHub Actions is the easiest CI to adopt and one of the most expensive to run at scale. The default Linux runner gives you two slow cores, and if you keep one busy around the clock it runs north of $300/month. Multiply that by a real test matrix and both the bill and the wall-clock time your engineers wait on get painful fast.
This is the written version of a talk I gave at a DevOps meetup in Rennes on May 16, 2024. It walks through why GitHub Actions is slow and expensive, what your actual alternatives are, and how I ended up building RunsOn to get faster runners for roughly a tenth of the cost, without leaving the GitHub Actions workflow syntax behind. The video covers the same ground if you’d rather watch:
What GitHub Actions gets right#
Credit where it’s due. GitHub Actions won for good reasons:
- It’s already there. Zero setup if your code is on GitHub. That ubiquity is most of the moat.
- The workflow syntax is flexible. Matrix builds, reusable workflows, and a huge marketplace of actions cover most of what teams need.
- Multi-architecture out of the box. Linux x64, macOS, and Windows runners, all hosted.
For a lot of projects that’s enough. The pain starts when CI becomes load-bearing.
Where it falls down#
- Weak, pricey defaults. Two cores, and they’re both slow and expensive. The hosted larger runners help with speed but make the cost problem worse, not better.
- Caching bottleneck. GitHub’s cache tops out around 100 MB/s. If your jobs push or pull large artifacts, that ceiling becomes the job.
- ARM is still beta. No GA ARM64 hosted runners yet, which slows down anyone building multi-arch images.
- You pay in engineering time, too. Slow machines push teams into endless test-suite tuning (sharding, parallelizing, trimming) that mostly evaporates the moment you run the same suite on stronger hardware.
The throughput and cost problems both point the same direction: stop renting GitHub’s machines and bring your own.
The fix: self-hosted runners#
A self-hosted runner is just a machine you control running GitHub’s runner agent. You get faster hardware at a lower price, and you decide the specs. There are three levels of how much of it you manage yourself:
| Approach | What it is | Pros | Cons |
|---|---|---|---|
| Artisanal on-prem | A handful of your own servers registered with GitHub | Cheap, simple to reason about | Limited concurrency, manual maintenance, security exposure, drifts from official runner images |
| Productized on-prem | Same idea, managed by tooling like Actions Runner Controller ↗ or the Philips Terraform module | Autoscaling, hardware flexibility | Needs real ops expertise; still not image-compatible without custom AMIs/Docker images |
| Third-party SaaS | A vendor runs the machines, you point your workflow at them | No maintenance, ~50% cheaper, usually official-image compatible | Your code and secrets run on their infra; fixed hardware menu (pick CPU count, not CPU type/disk/GPU); surcharges past ~64 concurrent CPUs; often hosted where the network is slow |
None of these is free of tradeoffs. The artisanal route doesn’t scale; the productized route is a second system to operate; the SaaS route asks you to hand your secrets to a third party and live with their hardware menu.
The wider market#
Roughly how the alternatives sort out:
- Third-party SaaS: a crowded, fast-moving field; a new vendor seems to appear every month.
- Fully on-prem: Actions Runner Controller, the Terraform provider, and AWS CodeBuild, which can run managed runners inside your own AWS account.
- Hybrid: you supply hardware in your infrastructure, the vendor runs the control plane.
When I went looking for something cost-effective and low-effort, the fully on-prem options were a pain to stand up, slow to start, and prone to long queues. CodeBuild works but is expensive and comes with its own constraints. Nothing hit the combination I wanted, so I built it.
Why I built RunsOn#
RunsOn is a self-hosted runner that lives entirely in your own AWS account. It’s free for individual and non-commercial use, with paid licenses for commercial projects. I had four hard requirements:
- Fast and cheap. Strong CPUs at spot prices.
- Effectively unlimited scale. Run as many concurrent jobs as your AWS quotas allow.
- Drop-in compatible. Change one line in a workflow and everything still works.
- Almost no maintenance. Managed services only; no cluster to babysit.
Here’s how each one actually works.
Fast and cheap#
Speed comes from the hardware. RunsOn runs on AWS for the breadth of instance types and for spot pricing, landing around a 3,000 single-thread PassMark, a different class of core than the default hosted runner.
Cost comes from EC2 spot instances, which can be up to 75% cheaper than on-demand. CI jobs are short-lived, which is exactly the workload spot is good for. RunsOn uses EC2’s Create Fleet API to draw from the least-interrupted spot pools, so the savings don’t come at the price of constant interruptions.
Scale, with honest queue times#
Every job gets its own ephemeral EC2 instance, which RunsOn terminates the moment the job finishes. There’s no warm pool to manage and no shared state between jobs. The only ceiling on concurrency is your AWS quota.
On queue time I’ll be straight with you: GitHub’s own runners start in about 12 seconds; RunsOn is around 30 seconds after tuning the base AMIs and provisioning EBS throughput. Slower than GitHub, faster than most third-party providers, and more importantly, stable. In production with customers like Alan, bursts of 100+ simultaneous jobs don’t blow the queue time out.
Drop-in compatible#
This was the hardest part to get right, and the reason I think most alternatives feel second-class: matching GitHub’s official runner images. GitHub publishes the Packer templates for their runner images targeting Azure, so I ported them to AWS AMIs and published the result. That’s what makes the migration a one-line change:
# Before: GitHub-hostedruns-on: ubuntu-latest
# After: RunsOn, on your own AWS accountruns-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64Same toolchain, same preinstalled software, your infrastructure.
Almost no maintenance#
The whole thing is managed services. One CloudFormation stack provisions an SQS queue, an SNS alert topic, CloudWatch logs and metrics, and a few S3 buckets. The RunsOn server itself runs on AWS App Runner, a genuinely cheap way to run a container. Each VM runs a small agent that configures the instance and registers it with GitHub. For a reasonable job volume, the entire control plane costs about $1–2/month.
The nice-to-haves#
The requirements above were the point. These are what make it pleasant day to day:
- Pick your hardware per job: CPU, RAM, and disk set right in the workflow label.
- ARM64 and AMD64: both supported. No macOS yet; that’s an Apple licensing problem, not a technical one.
- One-click install and upgrade via a CloudFormation template URL.
- Faster caching: an S3-backed cache that’s up to 5× faster than GitHub’s, plus Docker layer caching.
- Full control of the box: SSH access, custom AMIs to preload software, private networking, and static EIPs.
As of this talk, RunsOn has handled on the order of 500,000 jobs across users from solo projects to large teams.
What’s next#
Two things I want to build:
- Cost transparency: surface the real dollar (and energy) cost of CI back to the developers triggering it.
- Right-sizing reports: tell you whether your runners are over- or under-provisioned so you stop paying for cores you don’t use.
Wrapping up#
GitHub Actions is the right place to start. Once CI becomes load-bearing, the default runners stop making sense, and self-hosted runners (artisanal, productized, or SaaS) are the way out, each with real tradeoffs. RunsOn is my attempt at the on-prem option that’s actually fast, actually cheap, and doesn’t ask you to relearn your workflows.
Have a look at runs-on.com or the source at github.com/runs-on/runs-on ↗.