Rightsizing runners
Choose the right instance family, CPU, RAM, and disk for RunsOn runners — fast enough to cut wall-clock time without overpaying for idle capacity.
Rightsizing is picking the smallest runner shape that still runs your job fast. Because RunsOn launches real EC2 instances that you pay for by the second, a bigger box is often cheaper per job when it finishes proportionally faster — but only up to the point where your workload stops scaling.
The three levers are the instance family, the CPU/RAM target, and the disk. In Flex these are job labels; in Fleet they are baked into the Terraform runner catalog.
Family#
The family selects the EC2 instance family — the generation and class of hardware. Newer generations (e.g. c8i, m8i, r8g) are faster and frequently cheaper per unit of work than older ones.
- Compute-bound (compilation, tests): the
cclass (c8i,c8g) — high clock, lower RAM ratio. - Balanced (most builds): the
mclass (m8i,m8g). - Memory-bound (large test matrices, JVM, data): the
rclass (r8i,r8g). - arm64:
*gfamilies (Graviton) list about a quarter below x64 on-demand; on spot the gap depends on the size (see the pricing table). Use them when your toolchain has arm64 support. The default image is x64, so a Graviton family also needs an arm64 image, e.g.family=c8g/image=ubuntu24-full-arm64; selecting a*gfamily without an arm64 image fails with an architecture mismatch.
# Pin a familyruns-on: runs-on=${{ github.run_id }}/family=c8i/cpu=4
# Or widen the pool so Spot has more capacity to choose fromruns-on: runs-on=${{ github.run_id }}/family=c8i+m8i/cpu=4Older generations: cheaper per hour, slower per core#
7th-gen families (c7i, c7a, c7g and their m/r siblings) still work, and they usually cost less per hour. You pay for that in speed. At 16 vCPUs in us-east-1:
| 7th gen vs 8th gen | On-demand price | Single-thread CPU score |
|---|---|---|
c7i vs c8i | ~5% cheaper | 6% lower |
c7a vs c8a | ~5% cheaper | 25% lower |
c7g vs c8g | ~9% cheaper | 20% lower |
Spot prices move with demand, and an older generation is not always the cheaper one on spot, so check the pricing table (it has a generation toggle) and the EC2 CPU benchmark before switching.
The trade pays off for jobs that mostly wait on the network, a database, or a disk: a slower core barely changes their runtime. For compile- and test-heavy jobs it usually doesn’t. If a job’s runtime tracks CPU speed, a 25% lower score means it runs about a third longer, which more than cancels a 5% lower hourly price.
CPU and RAM#
Target a cpu (and optionally ram) instead of a fixed instance size, and RunsOn picks an instance that satisfies it within the chosen family:
runs-on: runs-on=${{ github.run_id }}/cpu=8/ram=16Guidance:
- Match
cputo your job’s actual parallelism. A single-threaded job gains nothing from 16 vCPUs. - Use a range (
cpu=4+16) when the exact size doesn’t matter — it expands the available pool. - Measure before sizing up: the runner metrics show CPU, memory, and I/O utilization per job, so you can see whether a job is actually CPU-, memory-, or I/O-bound.
Running out of memory#
Linux runners with 16 GiB of RAM or less, and tmpfs runners of any size since v3.5.0, get compressed swap in memory (zram), sized to the runner’s RAM. It uses no disk space and, apart from a bookkeeping table of under 1% of RAM, uses memory only for what it holds: memory the job has not used recently is compressed in place, so a job that slightly exceeds the runner’s RAM slows down a little instead of being killed. GitHub-hosted runners carry a 4 GiB swap file for the same reason. Ubuntu 24.04 and 26.04 images support it; RunsOn skips it when the image already configures swap.
A few tools behave differently with swap on: containers started with --memory but no --memory-swap can also use that much swap before being killed, and a Kubernetes node installed on the runner with kubeadm refuses to start. To turn swap off for a job, make sudo swapoff -a its first step.
At the end of each Linux job, the Complete runner step adds a warning annotation to the workflow run when the job:
- had a process killed by the kernel because the runner ran out of memory (Out of memory), or
- spent at least 5% of its run time, and at least 10 seconds, stalled waiting for memory (Memory pressure).
Either warning means the job needs more RAM: raise ram, or pick a memory-optimized family such as r8i or r8g.
When memory does run out, Linux runners rank the RunsOn agent and the GitHub runner’s listener below the job’s own processes and containers, so the kernel stops one of the job’s processes and the job fails in that step, with the warning above. If the runner itself is stopped anyway, GitHub reports the job as lost; the instance’s agent log then records Kernel OOM killer stopped the GitHub runner (see CloudWatch logs).
Disk#
Default EBS volumes are fine for most jobs. For I/O-heavy workloads (large Docker builds, big checkouts), the bottleneck is often disk, not CPU:
- Choose families with local NVMe storage for the fastest scratch space.
- Or tune the EBS volume (size, IOPS, throughput) via the
volumelabel. - For pure throughput on data that doesn’t need to survive the job, see YOLO mode (tmpfs).
Related#
- Spot pricing — run rightsized instances on Spot capacity for up to 90% savings.
- Warm pools — keep rightsized runners pre-warmed for sub-10s pickup.
- Runner metrics — measure utilization to validate your sizing.