Best practice · Runner & Queue

Right-size GitHub Actions runners without slowing CI

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.

How StarSling works

AI agents open the PR

StarSling agents inspect your workflow, apply this optimization, and open a reviewable PR automatically.

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.123

A branch-only runner-size comparison
# 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

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
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

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.

  • Read the Mastra customer story
    Customer story
    Merged 2026-07-03

    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.

  • Read the Mastra customer story
    Customer story
    Merged 2026-08-19

    perf(ci): run the four jobs with no install on the 2-vCPU runner, 2× 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.

    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.

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.

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.

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.

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.

Verify on your repo

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to audit and fix this practice in your own repo.

Prompt for your coding agent
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?

  1. List jobs by 30-day frequency, runner label, billed minutes, and current per-minute rate; rank the largest recurring cost first.

  2. Inspect the job's step durations and CPU/memory behavior. Prefer jobs dominated by checkout, setup, API waits, or a short single-threaded command.

  3. 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.

  4. 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.

Go further

One fix, all of them, or forever.

You have the prompt for this one practice. Here is how much further you can take it, each step doing more for you than the last.

  1. Fix this one thing

    Copy the prompt above

    Hand OPT66 to your coding agent and fix it in your repo today.

  2. Fix everything, once

    Install the ci-speedup skill

    One prompt audits your whole repo against all 73 ci-speedup patterns (this one plus 72 more) and hands your agent every fix at once. Open source, MIT, runs locally.

  3. Keep it fixed, forever

    Install the StarSling GitHub App

    Connect GitHub and the fixes stay applied as your CI evolves, with agents that keep inspecting your workflows and opening optimization PRs you review.

More best practices for GitHub Actions

Where to go next in the CI best-practices catalog.

All CI best practices

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. 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.

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.

Sources

1GitHub Actions · runner pricing (opens in new tab)

2GitHub Actions · configuration variables (opens in new tab)

3StarSling · Linux runner instance types (opens in new tab)

Last updated 2026-08-19