Skip to content
Latchkey LogoLatchkey home

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

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.

On 2 and 4 vCPU: threaded 26.1 to 13.2 s, 8 loops 1.90 to 0.96 s, serial 3.59 to 3.58 s, one loop 1.72 to 1.74 s
One script run twice, once at each size, on 2026-09-20 from job-j.sh. The only difference between the two runs is the size flag; the input file has the same sha256 in both logs.

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 image2 vCPU4 vCPUChange
xz -6 -T0, one thread per vCPU26.1 s13.2 s1.99x faster
Eight equal CPU loops, one per vCPU1.90 s0.96 s1.97x faster
Eight equal CPU loops, run one after another3.59 s3.58 sno change
One CPU loop, single process1.72 s1.74 sno change
xz -6 -T1, forced to one thread40.2 s37.7 snot claimed, see below
npm ci, 414 packages, cache emptied4.10 s3.48 s1.18x faster
npm ci, 414 packages, cache restored2.32 s1.99 s1.17x faster

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

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 logDiagnosisThe right move
99 percent CPU on one coreSingle-threaded critical pathA faster core, not more of them
Close to N times 100 percentGenuinely parallelMore vCPUs, in proportion
Well under 100 percentWaiting on network or diskCaching and concurrency, not size
Peak resident set near available memoryMemory boundA bigger size, for the RAM that comes with it
Exit code 137The kernel killed itMore RAM, urgently, and it is not a code bug

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 runnerRate per minute10,000 minutes a monthSpeedup needed to break even against 2 vCPU
2 vCPU (standard)$0.006$60baseline
4 vCPU$0.012$1202.0x
8 vCPU$0.022$2203.7x
16 vCPU$0.042$4207.0x
32 vCPU$0.082$82013.7x
64 vCPU$0.162$1,62027.0x
96 vCPU, x64 only$0.252$2,52042.0x

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.

Frequently asked questions

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.

Related guides

References

The break-even speedup is the rate ratio. Latchkey doubles the same way from $0.0025 at 2 vCPU. Start free → 30-day trial · No credit card