# GitHub Actions runner size: when 2, 4, 8 and 16 vCPU pay off

> Pick a GitHub Actions runner size from one reading in your log. The same work measured on 2 and 4 vCPU, with the rates that decide whether it pays.

Source: https://latchkey.dev/learn/speed/choose-a-github-actions-runner-size  
Updated: 2026-09-20

Choosing a GitHub Actions runner size is one question, not four: does the slow part of the job use more than one core. We ran the same script on a 2 vCPU and a 4 vCPU Latchkey runner, and the work that threads went from 26.1 seconds to 13.2 while the work that does not moved from 1.72 seconds to 1.74, on machines whose rate doubles between them.

Larger runners are the easiest change on this list to make and the easiest to waste money on, because the rate rises whether or not the job gets faster. GitHub prices a Linux runner at $0.006 a minute for 2 vCPU and $0.012 for 4, so a size you picked by feel is a bill you doubled by feel.

The question has a single, cheap, definitive answer that lives in your own log. One instrumented run tells you whether the slow step is saturating one core, spreading across all of them, or waiting on something neither a bigger machine nor a faster one will help with.

## The same work on 2 vCPU and on 4

We ran one script twice on Latchkey runners, once at `latchkey-small` with 2 vCPU and 7,734 MB of RAM and once at `latchkey-medium` with 4 vCPU and 15,617 MB, on the same AMD EPYC 7R13 processor and the same Ubuntu 24.04.4 image. Nothing in the script changed between the two runs, and the compressible input file it generates has the same sha256 in both logs, so the pair isolates vCPU count and nothing else.

The threaded rows almost exactly halved. The serial rows did not move at all. That is the whole decision, and it is worth seeing in the same table rather than as two separate claims, because every real pipeline is a mixture of the two and the mixture is what sets your answer.

| Work, control held: same script, input and image | 2 vCPU | 4 vCPU | Change |
| --- | --- | --- | --- |
| `xz -6 -T0`, one thread per vCPU | 26.1 s | 13.2 s | 1.99x faster |
| Eight equal CPU loops, one per vCPU | 1.90 s | 0.96 s | 1.97x faster |
| Eight equal CPU loops, run one after another | 3.59 s | 3.58 s | no change |
| One CPU loop, single process | 1.72 s | 1.74 s | no change |
| `xz -6 -T1`, forced to one thread | 40.2 s | 37.7 s | not claimed, see below |
| `npm ci`, 414 packages, cache emptied | 4.10 s | 3.48 s | 1.18x faster |
| `npm ci`, 414 packages, cache restored | 2.32 s | 1.99 s | 1.17x faster |

> Measured by `job-j.sh`, under content/repro/timings/choose-a-github-actions-runner-size/, run once at each size on 2026-09-20, with the two logs committed as `job-j-small.log` and `job-j-medium.log`. One pass per cell. The single-threaded `xz` row is not claimed as an improvement: the same command timed twice inside each job came back at 40.2 s and 40.8 s on the small runner and 37.7 s and 32.4 s on the medium one, so that measurement has a 14 percent spread of its own. The two clean single-threaded controls, the node loops, agree to within 1 percent.

## Read your own log instead of guessing

One step, added once, answers the question permanently. Run your slowest command under `/usr/bin/time -v` and print `nproc` beside it. The line to read is "Percent of CPU this job got", and it has exactly three interesting values.

Near 100 percent means one core is saturated and the rest are idle: a bigger machine changes nothing and a faster core would. Near N times 100 percent means the work is genuinely spread and more vCPUs buy you time in proportion. Well under 100 percent means the job is waiting on a network or a disk, and neither more cores nor faster ones will touch it.

```.github/workflows/ci.yml
- name: What is this job actually doing
  run: |
    nproc
    free -m | head -2
    /usr/bin/time -v ./your-slow-command 2>&1 | tail -25
```

> Our own logs read exactly this way. Forced to one thread, `xz` reported 99 percent on both sizes. Allowed all threads, it reported 199 percent on the 2 vCPU runner and 397 percent on the 4 vCPU one, which is the signature of work that will keep scaling.

## Memory is the other half of the size, and it moves with you

On every provider, buying vCPUs buys RAM at the same time, and that is not a bonus you can ignore: the parallelism you just paid for often needs it. The same compression run used a peak resident set of 214,580 KB with two threads and 431,532 KB with four. The memory demand doubled alongside the speed.

So read two numbers out of your log, not one. `free -m` reports what is available, which was 7,029 MB of 7,734 on the small runner and 14,832 MB of 15,617 on the medium one before the work started. If your peak resident set is a large fraction of that, the size you need is set by memory rather than by cores, and the symptom of getting it wrong is not a slow job, it is exit code 137 and no explanation in the build output.

| Reading in the log | Diagnosis | The right move |
| --- | --- | --- |
| 99 percent CPU on one core | Single-threaded critical path | A faster core, not more of them |
| Close to N times 100 percent | Genuinely parallel | More vCPUs, in proportion |
| Well under 100 percent | Waiting on network or disk | Caching and concurrency, not size |
| Peak resident set near available memory | Memory bound | A bigger size, for the RAM that comes with it |
| Exit code 137 | The kernel killed it | More RAM, urgently, and it is not a code bug |

> The CPU percentages and the resident set sizes are the `/usr/bin/time -v` lines in `job-j-small.log` and `job-j-medium.log`. GitHub documents the standard Linux runner as 2 vCPU with 8 GB of RAM on a private repository and 4 vCPU with 16 GB on a public one, which is why the same workflow can feel slower the week a repository is made private.

## What the extra vCPUs cost, and the line where they pay

GitHub publishes a per-minute rate for each Linux size, and it roughly doubles with the vCPU count up to 4 before turning slightly sublinear above it. The documentation is blunt about who pays, in two consecutive sentences: "Included minutes cannot be used for larger runners. The larger runners are not free for public repositories." So every minute is billed on a plan with allowance left, and an open source repository that pays nothing at all for standard runners starts paying the moment you size up.

That gives you a break-even you can apply without a spreadsheet. A size that doubles the rate has to at least halve the wall clock to cost you the same. Our threaded rows cleared that bar at 1.99x and were free speed. The `npm ci` rows came back at 1.18x for a 2x rate, which is a real improvement you are paying about 1.7 times as much for. The single-threaded rows were a pure doubling of the bill.

One detail that bites small jobs: GitHub rounds each job up to the nearest minute. A step that goes from 26 seconds to 13 still bills one minute, at twice the rate. Sizing up is worth doing on jobs long enough for the saving to cross a minute boundary, and worth nothing at all on the short ones.

| Linux runner | Rate per minute | 10,000 minutes a month | Speedup needed to break even against 2 vCPU |
| --- | --- | --- | --- |
| 2 vCPU (standard) | $0.006 | $60 | baseline |
| 4 vCPU | $0.012 | $120 | 2.0x |
| 8 vCPU | $0.022 | $220 | 3.7x |
| 16 vCPU | $0.042 | $420 | 7.0x |
| 32 vCPU | $0.082 | $820 | 13.7x |
| 64 vCPU | $0.162 | $1,620 | 27.0x |
| 96 vCPU, x64 only | $0.252 | $2,520 | 42.0x |

> Rates quoted from the Actions minute multipliers reference on docs.github.com, linked below and read on 2026-09-20. The break-even column is arithmetic on those rates, not a measurement. Larger Linux runners are documented at 2, 4, 8, 16, 32, 64 and 96 vCPU, from 8 GB to 384 GB of RAM; at 64 vCPU that is 208 GB on arm64 and 256 GB on x64, and 96 vCPU is x64 only.

## Size per job, not per workflow

Runner size is a per-job setting, and the most expensive mistake in this area is putting a whole workflow on a large runner because one job in it needed the memory. Your lint job is single-threaded and finishes in forty seconds; there is no version of the argument where it belongs on 16 vCPU.

Split by what the reading told you. The job whose CPU percentage sat at 99 stays small. The job that reported 397 percent and has more parallelism available goes up one step at a time, measuring each step, because the scaling stops the moment the work runs out of independent pieces and the rate does not.

```.github/workflows/ci.yml
jobs:
  lint:
    runs-on: ubuntu-latest          # 99% on one core, nothing to gain
  test:
    runs-on: ubuntu-latest-4-core   # sharded, scales with cores
  build:
    runs-on: ubuntu-latest-8-core   # parallel compile and memory hungry
```

## What we ran, so you can disagree with it

One script, `job-j.sh`, run twice on 20 September 2026, committed under content/repro/timings/choose-a-github-actions-runner-size/ with both unedited logs and a status file per run carrying the job id, the runner size and the exit code. The only difference between the two invocations is the size flag on the command line.

One pass per cell, so a difference under about half a second is not real, which is why the two serial rows are reported as no change rather than as a hundredth of a second either way. The single-threaded `xz` row is the one place the noise was large enough to see: repeated inside each job it varied by up to 14 percent, so we print it and decline to draw a conclusion from it rather than quietly leaving it out.

The two sizes also ran on the same processor model, which is the fair test of core count and not a test of core speed. A provider that pairs bigger sizes with faster cores would move the single-threaded rows too, and none of ours moved.

## FAQ

### Are larger GitHub Actions runners worth it?

Only when the slow step uses more than one core. On the same script at 2 and 4 vCPU, the threaded work halved and the single-threaded work did not move at all, while the published rate doubled. Run your slowest command under `/usr/bin/time -v` and read the CPU percentage: 99 percent means a larger size is a pure price increase.

### What do GitHub Actions larger runners cost?

GitHub publishes $0.006 a minute for Linux at 2 vCPU, $0.012 at 4, $0.022 at 8, $0.042 at 16, $0.082 at 32, $0.162 at 64 and $0.252 at 96, which is x64 only, read on 20 September 2026. Included minutes cannot be used for larger runners, so those minutes are billed even on a plan with allowance remaining, and each job is rounded up to the nearest minute.

### How much RAM does a GitHub-hosted runner have?

GitHub documents the standard Linux runner as 2 vCPU with 8 GB of RAM and about 14 GB of SSD on a private repository, and 4 vCPU with 16 GB on a public one. Larger Linux runners run from 8 GB at 2 vCPU to 384 GB at 96, which is x64 only; the 64 vCPU size is 208 GB on arm64 and 256 GB on x64. The public and private asymmetry is real and it is why the same workflow can slow down the week a repository changes visibility.

### Does a bigger runner make a single-threaded job faster?

Not on its own. More vCPUs give a single-threaded step more idle cores, not a faster one. Our clean single-threaded controls came back at 1.72 seconds on 2 vCPU and 1.74 on 4, a difference of under 1 percent, on the same processor model. A provider that also raises the clock with the size would change this, so check the processor model in both logs before assuming either way.

## References

- [GitHub Docs: Actions minute multipliers and per-minute rates (verified 2026-09-20)](https://docs.github.com/en/billing/reference/actions-minute-multipliers)
- [GitHub Docs: GitHub-hosted runner specifications (verified 2026-09-20)](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)
- [GitHub Docs: larger runners reference (verified 2026-09-20)](https://docs.github.com/en/actions/reference/runners/larger-runners)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
