---
title: "Right-size GitHub Actions runners without slowing CI"
description: "Choose a smaller GitHub Actions runner when a job cannot use extra cores. Compare the same commit, protect wall-clock, and cut runner cost safely."
url: https://starsling.dev/best-practices/github-actions/right-size-runners
canonicalUrl: https://starsling.dev/best-practices/github-actions/right-size-runners
---

# Right-size GitHub Actions runners without slowing CI

- [How StarSling works](https://starsling.dev/)

Best practice: Runner & Queue. Last updated: 2026-08-19

Right-size a GitHub Actions job by comparing the same commit on the current and smaller runner, then keep the smaller size only when pass rate and wall-clock remain equivalent. Short validation and orchestration jobs often cannot use extra cores, so a larger runner raises the per-minute rate without moving the merge gate.

## Table of contents

- [Do this: A/B the same job on both runner sizes](#do-this)
- [Avoid this: pay for cores the job cannot use](#avoid-this)
- [Shipped by StarSling](#shipped-by-starsling)
- [Measure runner size without fooling yourself](#measure-runner-size-without-fooling-yourself)
- [Separate the billing floor from the merge gate](#billing-floor-and-critical-path)
- [Why it matters](#why-it-matters)
- [When to right-size a runner](#when-to-use)
- [Verify on your repo](#verify)
- [Trying to make GitHub Actions faster?](#speedup-guides)
- [Sources](#sources)

<a id="do-this"></a>

## Do this: A/B the same job on both runner sizes

Runner size follows measured job behavior, not a repository-wide default. Test the same commit and command on the current runner and a smaller candidate, keep caches and inputs identical, compare a distribution of successful runs, and change the production label only when the candidate preserves pass rate and wall-clock. Short jobs that mostly wait on checkout, an API, or one single-threaded command are the strongest candidates.

_A branch-only runner-size comparison_

```yaml
# Set CI_BASELINE_RUNNER and CI_SMALL_RUNNER to real labels in
# Settings > Secrets and variables > Actions > Variables.
on:
  workflow_dispatch:

jobs:
  validate-runner-size:
    strategy:
      fail-fast: false
      matrix:
        runner:
          - ${{ vars.CI_BASELINE_RUNNER }}
          - ${{ vars.CI_SMALL_RUNNER }}
        # Ten samples per arm expose normal variance instead of comparing two lucky runs.
        sample: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    name: ${{ matrix.runner }} · sample ${{ matrix.sample }}
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run validate-package-json
```

<a id="avoid-this"></a>

## Avoid this: pay for cores the job cannot use

A short or single-threaded job pays the larger runner's rate while checkout, network waits, and one command leave the extra cores idle.

_One oversized default for every job_

```yaml
jobs:
  validate-package-json:
    # This job checks out, sets up Node, and runs one short validator.
    # A larger runner is inherited without evidence that the job uses it.
    runs-on: ${{ vars.CI_LARGE_RUNNER }}
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: 22
      - run: npm run validate-package-json
```

<a id="shipped-by-starsling"></a>

## Shipped by StarSling

Mastra had two generations of the same measured opportunity: short validation and orchestration jobs running on a 4-vCPU label even though their work could not use the extra cores. StarSling agents moved them to the 2-vCPU tier only after same-commit comparisons showed no meaningful wall-clock penalty.

### mastra-ai/mastra#18871: perf(ci): move validate-pkg-json to the 2-vCPU StarSling runner, same wall-clock

The validate-pkg-json job does only a checkout, setup-node, and one validator script with no CPU-parallel install. A 10-runs-per-arm comparison found no meaningful wall-clock difference, so StarSling moved it from the 4-vCPU runner to the 2-vCPU tier and halved its per-minute rate.

Diff: lint.yml changes validate-pkg-json from starsling-ubuntu-24.04 to starsling-ubuntu-24.04-2; the steps and gate decision stay the same.

Source: [mastra-ai/mastra#18871](https://github.com/mastra-ai/mastra/pull/18871), merged 2026-07-03.
Customer story: [Mastra](https://starsling.dev/customers/mastra).

### mastra-ai/mastra#21824: perf(ci): run the four jobs with no install on the 2-vCPU runner, 2x cheaper

StarSling extended the measured right-size to four jobs that start no install, build, or test. A 50-run same-commit A/B found no measurable time penalty, while the 2-vCPU tier cut the per-minute rate in half.

Diff: lint.yml, major-version-check.yml, prebuild.yml, and secrets.test-workspaces.yml move four jobs to starsling-ubuntu-24.04-2; check-changes also adds filter: blob:none to avoid making its checkout CPU-bound on the smaller runner.

The blobless checkout is a separate supporting optimization. The right-sizing claim comes from the four runner-label changes and the A/B evidence, not from partial clone alone.

Source: [mastra-ai/mastra#21824](https://github.com/mastra-ai/mastra/pull/21824), merged 2026-08-19.
Customer story: [Mastra](https://starsling.dev/customers/mastra).

<a id="measure-runner-size-without-fooling-yourself"></a>

## Measure runner size without fooling yourself

Use the same commit, command, dependency versions, cache state, and runner pool for both arms. Compare successful-run medians and tails across enough repetitions to include normal variance, and inspect CPU and memory before interpreting the result. A smaller runner is safe only when it preserves the job's pass rate and its place on the critical path. One lucky run is not an A/B test.

<a id="billing-floor-and-critical-path"></a>

## Separate the billing floor from the merge gate

GitHub bills hosted-runner time in whole-minute increments, so a job that finishes in seconds can cost a full minute on either size. Right-sizing lowers cost when the smaller SKU has a lower rate and the billed-minute count stays the same. It does not justify slowing a merge-gating build or test: if the smaller runner moves the workflow's critical path, keep the larger runner and optimize the work instead.

<a id="why-it-matters"></a>

## Why it matters

Runner cost is rate multiplied by billed time. Extra cores help only when the job has parallel CPU work that can use them; they do little for API calls, checkout, fixed network waits, or a short single-threaded validator. Measuring job by job avoids both failure modes: paying for idle capacity everywhere, or shrinking a compute-bound job until developer wait gets worse.

<a id="when-to-use"></a>

## When to right-size a runner

**Use it when:** Start with frequent jobs whose own command finishes quickly, whose billed time is already at the one-minute floor, or whose step trace shows most time in checkout, setup, or external waits rather than parallel compute.

**Be careful when:** Do not shrink CPU-bound builds, parallel test suites, memory-heavy jobs, Docker builds, or any merge-gating job whose candidate arm has slower tails, more failures, or resource pressure. A cheaper minute is not cheaper if the job needs more minutes or more reruns.

<a id="verify"></a>

## Verify on your repo

Hand this prompt to your coding agent to audit and fix this practice in your own repo:

Audit this repository's GitHub Actions runner sizes job by job. Use recent successful job history to identify frequent jobs whose own work is short or mostly waits on checkout, setup, APIs, or one single-threaded command, then compare their current runner label and per-minute rate with a smaller same-platform candidate. Do not assume smaller is safe from YAML alone. For the best candidate, create a branch-only A/B that runs the same commit, commands, cache state, and inputs on both labels; compare pass rate, median, P95, billed minutes, and any CPU or memory pressure across repeated runs. Change the production runs-on label only if the smaller arm preserves the merge gate and does not add failures, OOMs, timeouts, or reruns. Leave CPU-bound builds, parallel tests, memory-heavy jobs, and Docker builds unchanged unless the data proves equivalence. Show the evidence, choose only the highest-confidence job, and open a PR rather than applying it blindly.

Ground these changes in the upstream docs before you edit: https://docs.github.com/en/billing/reference/actions-runner-pricing, https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-variables, https://docs.starsling.dev/runners/instance-types. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.

Prefer to check by hand:

- List jobs by 30-day frequency, runner label, billed minutes, and current per-minute rate; rank the largest recurring cost first.
- Inspect the job's step durations and CPU/memory behavior. Prefer jobs dominated by checkout, setup, API waits, or a short single-threaded command.
- Run the same commit on the baseline and smaller labels with identical inputs and cache state, then compare successful-run median, P95, pass rate, and billed minutes.
- Change only the production `runs-on` label, then watch several normal PRs for wall-clock regression, OOMs, timeouts, and reruns before expanding the change to another job.

<a id="more-best-practices"></a>

## More best practices for GitHub Actions

- [Cut CI queue time in GitHub Actions](https://starsling.dev/best-practices/github-actions/cut-queue-time)
- [Shard tests across parallel jobs in GitHub Actions](https://starsling.dev/best-practices/github-actions/shard-tests)
- [Bound GitHub Actions runner jobs with timeout-minutes](https://starsling.dev/best-practices/github-actions/bound-job-timeouts)
- [All CI best practices](https://starsling.dev/best-practices/github-actions)

<a id="speedup-guides"></a>

## Trying to make GitHub Actions faster?

If you do not know why a run is slow yet, start with the diagnostic guide. [Let an agent find which of these applies: /ci-speedup](/ci-speedup). If your workflows are already optimized but still slow, see our guide to fast GitHub Actions and GitHub Actions runner alternatives. Building containers in CI? The Docker workflow guide covers layer caching end to end. Want to see how your configuration measures up before changing anything? CI Score grades workflow config against a pass/fail rubric of these practices. It is not a speed measurement.

- [GitHub Actions too slow](https://starsling.dev/github-actions-too-slow)
- [Fast GitHub Actions](https://starsling.dev/fast-github-actions)
- [GitHub Actions alternatives](https://starsling.dev/github-actions-alternatives)
- [GitHub Actions pricing](https://starsling.dev/github-actions-pricing)
- [Self-hosted GitHub Actions runners](https://starsling.dev/self-hosted-github-runners)
- [Docker CI on GitHub Actions](https://starsling.dev/ci/docker)
- [Install the free /ci-score skill to improve your GitHub Actions setup](https://starsling.dev/ci-score)

<a id="faq"></a>

## FAQ

### How do I know whether a GitHub Actions runner is oversized?

Look for a frequent job whose own command is short or mostly waits on checkout, setup, or an external API, then confirm with repeated same-commit runs on a smaller candidate. Low CPU use is a lead, not the verdict; pass rate, median, P95, billed minutes, memory pressure, and critical-path impact decide the change.

### Will a smaller runner always make the job slower?

No. A CPU-parallel build or test usually benefits from more cores, but a short validator, API call, or single-threaded command may not. The only safe answer is an A/B with identical work. Keep the smaller size only when the time distribution and reliability remain equivalent.

### Is runner right-sizing the same as reducing queue time?

No. Queue time is the wait before a runner starts the job; right-sizing changes the rate and capacity of the runner after it starts. A smaller runner can lower cost without changing queue time, while more available concurrency can lower queue time without changing runner size.

<a id="sources"></a>

## Sources

1. [GitHub Actions - runner pricing](https://docs.github.com/en/billing/reference/actions-runner-pricing)
2. [GitHub Actions - configuration variables](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-variables)
3. [StarSling - Linux runner instance types](https://docs.starsling.dev/runners/instance-types)
