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
- name: Profile the job
run: |
/usr/bin/time -v ./your-build-command 2>&1 | tail -25
echo "--- cores available ---"; nproc
echo "--- memory ---"; free -hWhat each reading means
| Reading | Diagnosis | Right move |
|---|---|---|
| ~100% CPU, 1 core | Single-threaded critical path | Faster core, not more cores |
| ~N x 100% CPU | Genuinely parallel | More vCPUs will help proportionally |
| Well under 100% | I/O or network bound | Cache and concurrency, not size |
| High peak memory, low CPU | Memory bound | More RAM, which usually means more vCPU |
| Exit code 137 | Killed by the OOM reaper | More RAM, urgently. Not a code bug |
The price of guessing
| Linux size | Rate | 10,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
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 hungryMeasure 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.
# 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 -40Frequently asked questions
Are larger GitHub Actions runners worth it?
/usr/bin/time -v and read the CPU percentage before upgrading.