RunsOn: an EC2-per-job runner control plane for GitHub Actions on AWS
Self-hosted GitHub Actions runners made simple. For AWS. 10x cheaper, up to 2x faster, and unlimited caching. Best alternative to Actions Runner Controller.
At a glance
- What is it?
- RunsOn replaces GitHub-hosted runners with ephemeral EC2 instances launched inside your own AWS account, configured either per job (Flex) or from a platform-team catalog (Fleet). The interesting question is not whether it is cheaper than GitHub-hosted minutes, but whether you want to own the AWS boundary that comes with it.
- Who is it for?
- Adopt RunsOn if your CI already runs on AWS and you want per-job EC2 isolation without operating a Kubernetes controller, an autoscaler, or your own runner-image pipeline. Do not adopt it if your infrastructure is Kubernetes-first, if you cannot accept spot interruption as a normal event even with on-demand fallback, or if you are unwilling to own IAM, VPC, AMI refresh, and the AWS bill.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- GitHub does not report a main language for this repository.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem RunsOn targets: the runner layer between GitHub and your compute
GitHub Actions splits into two halves. GitHub schedules and dispatches jobs; something else has to turn a queued job into a machine that runs it. With GitHub-hosted runners you rent that second half from GitHub, and you inherit its hardware catalog, its per-minute pricing, and its execution boundary. With self-hosted runners you own the second half, which historically means owning a runner platform: a controller, an autoscaler, image builds, and cache plumbing.
RunsOn is aimed at teams that want the second option without building the platform. The README describes it as a control plane for AWS that launches an isolated EC2 runner for every job, with Linux runner-image compatibility. The audience is platform and infrastructure teams already spending on AWS, and the pitch is that the control plane, runners, cache, credentials, and bill all stay in your account under your IAM and network controls.
Who it is not for is equally clear from the material. Nothing in the README suggests support for clouds other than AWS. The topic list and the installation links are all AWS: EC2 spot, CloudFormation, gp3 volumes, S3. If your workloads live in GCP or Azure, this project does not address you.
Flex labels: the runner shape is written into the workflow file
The core mechanism in the Flex deployment model is a single runs-on string that carries the whole machine specification. The README gives this example:
runs-on=${{ github.run_id }}/family=c8a+m8a/cpu=4/ram=8+16/image=ubuntu24-full-x64/extras=s3-cache/volume=100gb:gp3
Read that left to right and you have the data flow. The run ID keeps the job's runner identity unique per workflow run. family=c8a+m8a offers two instance families as alternatives, so the control plane can pick whichever has capacity. cpu and ram are ranges, not fixed values. image names a GitHub-compatible full Ubuntu image. extras=s3-cache attaches the built-in cache. volume=100gb:gp3 sets root storage. Other jobs, the README states, can request a GPU, ARM64, a custom AMI, a static IP, or private networking through the same label syntax.
This is the design decision worth noticing. Runner selection lives with the workflow author, which means a developer can choose an instance family and a disk size in a YAML file. That is convenient and it is also a governance surface: the label is the policy. The README's answer to that is Fleet, covered below, but Flex on its own puts hardware choice in the pull request.
Fleet: moving runner selection from workflow authors to a Terraform catalog
Fleet inverts the Flex model. The platform team declares named runner shapes and capacity limits as Terraform inputs, and workflow authors select a published name instead of describing hardware. The README's example defines a linux-build runner with cpu = 4, ram = [8, 16], family = ["c8a", "m8a"], image = "ubuntu24-full-x64", and extras = ["s3-cache"], then a fleet that binds that runner to a timezone, a runner group, and max_runners = 200.
The workflow side collapses to runs-on: runs-on/fleet=linux-build/env=production. No CPU count, no family list, no volume size. The README states plainly that only the Terraform catalog can change the fleet's CPU, image, cache, runner group, or capacity. That is the whole point: the catalog is the contract, and a workflow cannot widen it.
Two operational details matter here. First, Flex and Fleet are separate control-plane deployments that can share one AWS account and one license, so a team can standardize common workloads on Fleet and drop to Flex for jobs that need something unusual. Second, the README labels Fleet as early access and tells you to pin an exact Terraform module version before rolling it out broadly. Treat that as the project's own statement about stability, not as marketing caution.
Deployment path: CloudFormation or Terraform, then one workflow change
The README's getting-started sequence is three steps. Choose the operating model. Deploy into your AWS account, using the CloudFormation install for Flex, the Terraform/OpenTofu module published on the Terraform Registry as runs-on/runs-on/aws, or the Fleet installation guide. Then connect GitHub and move one job.
The GitHub connection differs by model: the README specifies a private GitHub App setup for Flex and an enterprise PAT setup for Fleet. That difference is worth flagging before you plan a rollout, because a private App is an organization-level install while a PAT is a credential with a lifetime and a rotation obligation. If your security posture rejects long-lived PATs, Fleet's connection method is a constraint you need to resolve first.
Migration itself is deliberately narrow. The README's instruction is to update the workflow's runs-on value. A job that previously read runs-on: ubuntu-latest becomes a RunsOn label string. Everything after the runs-on line, the checkout step and the build commands, is unchanged, and the README's compatibility claim rests on Linux AMIs built from GitHub's full runner images so existing actions and toolchains keep working. That claim is the one to test on your own workflows, since it is a claim about image contents, not about the control plane.
Cost and performance claims: what the README publishes and what it does not
The README publishes specific numbers, and they should be read as vendor-published comparisons rather than independent measurements. It states 7x lower 2-vCPU Linux cost at $0.0009/min on c8a.large versus $0.0060/min on GitHub-hosted Linux, up to 13x lower T4 GPU cost at $0.0041/min on g4dn.xlarge versus $0.0520/min, and up to 89% higher single-thread CPU performance with m8azn scoring 4,299 against 2,269. A footnote states that prices are US East spot comparisons with a 30 GB gp3 root volume, that CPU figures are PassMark single-thread scores, and that AWS prices change, directing readers to a live calculator for their region and workload.
Three qualifiers are doing real work in that footnote. Spot pricing is the basis of the cost comparison, so the headline ratio depends on spot availability in your region and on your tolerance for interruption. The comparison assumes a 30 GB gp3 root volume. And the performance figure is a single-thread synthetic score, which says nothing about your build's actual bottleneck, whether that is I/O, network, or a compiler that parallelizes well.
The README also cites a scale figure, 2.13M jobs in one day with 24.7 fresh runners per second sustained, attributed to customers. It is a vendor-published number. The useful part is not the magnitude but the implication of the architecture: if runners are per-job and scale to zero, then runner launch rate is the throughput ceiling, and that is the number to think about when sizing a large migration.
Caching and storage: four different mechanisms with different failure modes
The README lists four ways to reuse build work: a built-in S3 cache, a Docker pull-through cache, EFS, and EBS sticky disks. These are not interchangeable, and the README does not present them as such.
The S3 cache appears as extras=s3-cache in a label, which makes it per-job opt-in. The Docker pull-through cache addresses image pulls specifically. EFS and EBS sticky disks are shared or persistent storage at the AWS level, which means they come with the usual properties of those services: EFS is a network filesystem with its own throughput and cost profile, and an EBS sticky disk ties a job to a volume that has to live somewhere between runs.
Because runners are ephemeral by design, every caching choice is really a choice about what survives the runner. S3 and the pull-through cache are external to the instance and survive trivially. EFS survives but adds a mount and a network dependency to every job. EBS sticky disks survive in a way that implies state management the README does not detail in the supplied material. If your build relies on a warm local cache directory, that is the assumption to re-examine before migrating, because a fresh EC2 instance per job does not carry one.
Spot instances, on-demand fallback, and where RunsOn is the wrong tool
The README frames spot usage carefully: use spot instances with on-demand fallback, and the stated goal is to cut compute costs without making jobs fragile. That phrasing is an admission that spot interruption is a real event, not a theoretical one. A job running on a reclaimed instance has to be retried, and a retried job costs both time and, for long builds, developer attention. The fallback exists precisely because the cheap path is not always available.
The second limitation is the AMI refresh cycle. The README states that Linux AMIs are built from GitHub's full runner images and refreshed every 15 days. That cadence is what keeps toolchains current, and it is also an upstream dependency: a change in the base image can change what your workflow sees. The README offers bring your own AMI as the escape hatch, which is the right control to have, but it means someone owns image builds if you take it.
The third is scope. RunsOn is AWS-only per the material, and it is a runner control plane, not a CI system. It does not replace GitHub Actions scheduling, secrets handling, or workflow syntax. If your problem is that Actions itself is the wrong CI engine, RunsOn does not address it. If your problem is that you want a Kubernetes-native runner platform because your workloads already live in a cluster, RunsOn is explicitly not that; the README positions ARC as the choice for Kubernetes-first teams who accept operating the cluster, controller, autoscaling, and runner images.
RunsOn against Actions Runner Controller: different things to operate
The README's own comparison is the clearest statement of the difference. RunsOn keeps the control plane, runners, cache, credentials, and bill in your AWS account and handles the runner lifecycle, and the README says it does so without adding a Kubernetes cluster, controller, autoscaler, or runner-image pipeline. ARC is described as fitting Kubernetes-first teams that want runner pods beside other cluster workloads, with the team operating the cluster, controller, autoscaling, and runner images.
So the trade is not cost against cost. It is which operational surface you already have. If you run Kubernetes and have people who run it well, ARC reuses that competence and puts runners where your other workloads are. If you do not run Kubernetes, ARC asks you to acquire a cluster and the skills to operate it, which is a large fixed cost before the first job runs. RunsOn asks you to acquire AWS competence instead, which most teams pursuing this comparison already have, and it removes the cluster from the job path entirely.
The runner-image pipeline is the second axis. ARC leaves image building to you. RunsOn ships GitHub-compatible AMIs on a 15-day refresh and lets you substitute your own. That is a genuine reduction in ongoing work, and it is also a dependency on someone else's image decisions, which is why the bring-your-own-AMI path matters more than it first appears.
Licence, maintenance, and what the release cadence tells you
The repository is MIT licensed. That covers the code in the repository. It does not automatically cover the AMIs, the published Terraform module, the CloudFormation templates, or any hosted service the vendor operates, and the README's language about sharing one license between Flex and Fleet deployments implies a licensing model beyond the MIT grant. Anyone planning a production rollout should confirm what the licence covers for the artifacts they actually deploy rather than assuming the repository's MIT file settles it. This is a question for the vendor or a lawyer, not something to infer from a README.
On maintenance, the release history in the supplied material shows v3.3.1 on 2026-09-10, v3.3.0 two days earlier, and v3.2.3 on 2026-08-24. Patch releases at that spacing are consistent with an actively maintained project, and the 3.x version line suggests the interface has been through at least three major revisions. The README's own warning to pin an exact Terraform module version before rolling Fleet out broadly is the practical takeaway: in a Terraform or CloudFormation deployment, the module version is your upgrade boundary, and a floating version means the control plane can change under you.
The upgrade cost itself is not described in the supplied material. There is no upgrade guide, no deprecation policy, and no statement about whether Flex label syntax is stable across minor versions. That absence is itself information: budget time to read the release notes before each bump rather than assuming label compatibility.
Editorial conclusion
Adopt RunsOn if your CI already runs on AWS and you want per-job EC2 isolation without operating a Kubernetes controller, an autoscaler, or your own runner-image pipeline. Do not adopt it if your infrastructure is Kubernetes-first, if you cannot accept spot interruption as a normal event even with on-demand fallback, or if you are unwilling to own IAM, VPC, AMI refresh, and the AWS bill. Before moving more than one workflow, verify three things: that the Flex label syntax covers the CPU, RAM, image, and volume combinations your jobs actually need; that the private GitHub App permissions match your organization's policy; and that the runner-image AMIs, which the README says are refreshed every 15 days, can be pinned or replaced with your own AMI so an upstream refresh cannot silently change your toolchain.
Community notes