Skip to content
Latchkey

How to Choose the Right GitHub Actions Runner Size

Doubling vCPUs doubles the per-minute rate. It only halves the runtime if the work is actually parallel, and most CI critical paths are not.

Larger runners are the easiest CI change to make and the easiest to waste money on. GitHub prices roughly linearly with vCPU count, so a 4-core runner costs about twice a 2-core one. If your job is single-threaded, you have doubled the bill and changed nothing.

Measure first. One instrumented run tells you whether the job is CPU-bound, parallel, memory-bound, or waiting on I/O, and each answer points at a different size.

Measure what the job is actually doing

.github/workflows/ci.yml
- name: Profile the job
  run: |
    /usr/bin/time -v ./your-build-command 2>&1 | tail -25
    echo "--- cores available ---"; nproc
    echo "--- memory ---"; free -h

What each reading means

ReadingDiagnosisRight move
~100% CPU, 1 coreSingle-threaded critical pathFaster core, not more cores
~N x 100% CPUGenuinely parallelMore vCPUs will help proportionally
Well under 100%I/O or network boundCache and concurrency, not size
High peak memory, low CPUMemory boundMore RAM, which usually means more vCPU
Exit code 137Killed by the OOM reaperMore RAM, urgently. Not a code bug

The price of guessing

Linux sizeRate10,000 min/month
2-core (standard)$0.006/min$60
4-core (larger)$0.012/min$120
8-core (larger)$0.022/min$220
16-core (larger)$0.042/min$420

Size per job, not per workflow

.github/workflows/ci.yml
jobs:
  lint:
    runs-on: ubuntu-latest        # small: single-threaded, quick
  test:
    runs-on: ubuntu-latest-4-core # parallel test runner
  build:
    runs-on: ubuntu-latest-8-core # parallel compile, memory hungry

Measure before you optimise

Pipeline optimisation usually targets the step people assume is slow. Get the real per-step timings first, because the answer is frequently dependency install or a cold cache rather than the build itself.

Terminal
# per-job timings for the last 20 runs
gh run list --limit 20 --json databaseId,conclusion,createdAt,updatedAt \
  --jq '.[] | "\(.conclusion)\t\(.createdAt)\t\(.updatedAt)"'

# per-step timing inside one run
gh run view <run-id> --log | grep -E "^\S+\s+.*Run |##\[group\]" | head -40

Frequently asked questions

Are larger GitHub Actions runners worth it?
Only when the job is genuinely parallel or memory-bound. Rates scale roughly linearly with vCPU, so doubling cores on a single-threaded job doubles cost and saves nothing. Profile with /usr/bin/time -v and read the CPU percentage before upgrading.
How much RAM does a GitHub-hosted runner have?
On a private repository, the standard Linux runner is 2 vCPU with 8 GB RAM and about 14 GB of free disk. On a public repository it is 4 vCPU with 16 GB. That asymmetry is why the same workflow can feel slower after a repository goes private.
What does exit code 137 mean in CI?
The kernel killed the process, almost always for memory. It is not an application error and nothing in the build log will explain it. You need more RAM, which on most providers means a larger runner size.
Do larger runners use my included minutes?
No. GitHub bills larger runners even when included minutes remain and even on public repositories. Only standard runners draw on the included allowance.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card