Sizing Runners for Rust Builds

Size Rust jobs by how wide the cargo dependency graph stays. 4 vCPU for check jobs, 8 to 16 vCPU for tests, and the point where extra cores stop paying.

Last verified:

Most Rust jobs land on 8 to 16 vCPU: warp-ubuntu-latest-x64-8x at $0.016 per minute for a workspace whose compile set stays under about 150 crates, and warp-ubuntu-latest-x64-16x at $0.032 once it passes that or compiles dependencies cold. The number that picks the size is how wide the cargo dependency graph stays rather than how many crates the workspace holds, because cores only pay while independent crates are ready to compile at the same time.

This guide covers the two properties that set Rust build parallelism, a table mapping compile-set size and codegen units to a runner label, the point where extra cores stop removing wall clock, and a workflow that matches the job count and the cache paths to the label you picked.

Diagnosis

Cargo compiles a directed acyclic graph. A crate becomes ready when every crate it depends on has finished, so the parallelism available at any instant is the size of the ready set, and the ready set changes shape across the run.

Two properties decide how wide it gets.

Graph width. A workspace with 700 registry dependencies has a wide middle: hundreds of leaf crates with no dependents compile at once, and any core you add during that phase does real work. The ends are narrow. At the start only the leaves are ready, and at the end the final workspace crates plus their test binary links depend on nearly everything before them.

Codegen units. Within one crate, rustc splits code generation into parallel units. The dev profile defaults to 256 codegen units and the release profile to 16, per the Cargo profile reference. Setting codegen-units = 1 or enabling fat LTO collapses that to a single serial step, which is why a release job tuned for runtime speed scales worse than a debug job on the same workspace.

Measure both with cargo build --timings, which writes an HTML report including a concurrency graph of units active, waiting on a dependency, and waiting on a core (Cargo timings reference). Two readings settle the size question. If units waiting on a core stay high, the runner is undersized. If active units sit below your vCPU count for most of the run, you are paying for idle cores.

Memory is the second constraint. Every size on the Linux x64 ladder carries 4 GB per vCPU and a 150 GB SSD, listed in the cloud runners documentation, so parallel rustc processes on a crate with a large generic surface can exhaust RAM before they exhaust CPU. An aarch64-unknown-linux-gnu target compiles natively on warp-ubuntu-latest-arm64-8x at $0.012 per minute.

Fix

Pick the label from the compile set and the codegen units in play, then confirm against a timings report.

Job shapeCrates in the compile setCodegen units in playLabelPrice per minute
cargo fmt, cargo clippy, cargo check with a warm dependency cacheup to 60 rebuiltnone; check skips code generationwarp-ubuntu-latest-x64-4x$0.008
cargo test, dev profile, warm dependency cache100 to 150256 per cratewarp-ubuntu-latest-x64-8x$0.016
cargo test on a wide workspace, or any cold dependency compile150 to 600256 per cratewarp-ubuntu-latest-x64-16x$0.032
Release build with thin LTO across a large graph600 or more16 per cratewarp-ubuntu-latest-x64-32x$0.064
Release build with fat LTO or codegen-units = 1any1, in one final serial stepwarp-ubuntu-latest-x64-8x$0.016

Rates come from the pricing page. The last row is the one teams get wrong: a fat-LTO release job serializes the expensive half of the work, so the 32 vCPU rate buys idle cores. Size release and test jobs separately when the release profile turns LTO on.

Where extra cores stop helping

Split the run into a parallel phase and a serial tail. The parallel phase divides by core count; the tail does not move. Take a workspace of 44 first-party crates and 720 registry dependencies with 96 CPU-minutes of compile work, of which 88 CPU-minutes sit in the parallel phase and 3.20 minutes of wall clock form the tail of final crates and test binary links.

LabelvCPUParallel phaseSerial tailWall clockCost per runCost per minute of wall clock removed
warp-ubuntu-latest-x64-4x422.00 min3.20 min25.20 min$0.2016baseline
warp-ubuntu-latest-x64-8x811.00 min3.20 min14.20 min$0.2272$0.0023
warp-ubuntu-latest-x64-16x165.50 min3.20 min8.70 min$0.2784$0.0093
warp-ubuntu-latest-x64-32x322.75 min3.20 min5.95 min$0.3808$0.0372

The model assumes the ready set stays wide enough to fill every core during the parallel phase, which is the optimistic bound; a real graph narrows earlier. Even so, the tail is 23 percent of the 8 vCPU run and 54 percent of the 32 vCPU run, and the last doubling costs $0.0372 per minute of wall clock removed against $0.0023 for the first. Substitute your own two numbers from a timings report before moving up a size.

The Observability page closes the loop after the change. Its recommendations view groups runs by repository, workflow, job, and instance type, and reports maximum CPU, memory, filesystem, disk I/O, and network utilization per instance, flagging jobs that are under-provisioned or over-provisioned. CI observability sits alongside snapshot runners, remote Docker builders, an MCP server, and the Action Debugger in the product surface.

Configuration

Set the job count to the label, keep the cache scoped to what a job of that size actually reuses, and leave incremental state out of it.

name: rust

on:
  pull_request:

env:
  CARGO_TERM_COLOR: always
  CARGO_INCREMENTAL: "0"
  CARGO_BUILD_JOBS: "16"
  CARGO_PROFILE_TEST_CODEGEN_UNITS: "256"

jobs:
  check:
    runs-on: warp-ubuntu-latest-x64-4x
    steps:
      - uses: actions/checkout@v5
      - run: cargo clippy --workspace --all-targets -- -D warnings

  test:
    runs-on: warp-ubuntu-latest-x64-16x
    steps:
      - uses: actions/checkout@v5

      - name: Restore cargo home and compiled dependencies
        uses: WarpBuilds/cache@v1
        with:
          path: |
            ~/.cargo/registry/index
            ~/.cargo/registry/cache
            ~/.cargo/git/db
            target/debug/deps
            target/debug/build
            target/debug/.fingerprint
          key: rust-16x-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: rust-16x-

      - name: Test
        run: cargo nextest run --workspace --test-threads 16

CARGO_BUILD_JOBS matches the 16 vCPU label. Cargo already defaults to the logical CPU count (Cargo configuration reference), so the value is written down for two reasons: a matrix that runs the same job on more than one label makes the mismatch visible, and a workspace whose linker peaks past 4 GB per unit gets capped here instead of by moving up a size.

Four cache paths carry the reuse and one is deliberately absent. ~/.cargo/registry/index, ~/.cargo/registry/cache, and ~/.cargo/git/db remove the download. target/debug/deps, target/debug/build, and target/debug/.fingerprint hold the compiled dependency artifacts, which is the wide part of the graph the 16 vCPU label exists to compile. target/debug/incremental stays out, because it is large, it is billed as transfer minutes in both directions, and cargo discards it once checkout rewrites the workspace mtimes. The guide to sccache on GitHub Actions covers the object-cache route for the same reuse, and the answer on why cargo rebuilds everything works through the fingerprint check.

The cache key carries the label. A restore written by a 16 vCPU job is valid on any size, but keying by label keeps a size experiment from mixing artifacts with your steady state while you compare timings.

Cost or Time Model

Take the wall clock above at 900 pull request runs per month and price each rung, with rates from the pricing page:

LabelWall clock per runMonthly minutesMonthly cost
warp-ubuntu-latest-x64-8x14.20 min12,780$204.48
warp-ubuntu-latest-x64-16x8.70 min7,830$250.56
warp-ubuntu-latest-x64-32x5.95 min5,355$342.72

Moving from 8x to 16x costs $46.08 per month and removes 5.50 minutes from every run. Moving from 16x to 32x costs another $92.16 and removes 2.75 minutes. The second trade is worth making only when a check is blocking a merge queue.

Against GitHub-hosted list prices for the same shapes, from the GitHub Actions billing reference, checked on 2026-08-13:

  • warp-ubuntu-latest-x64-8x (8 vCPU, 32 GB) costs $0.016 per minute against $0.022 per minute for the 8-core Linux larger runner (8 vCPU, 32 GB): 27 percent lower list price. The 12,780 monthly minutes above come to $281.16 on the GitHub-hosted runner against $204.48.
  • warp-ubuntu-latest-x64-32x (32 vCPU, 128 GB) costs $0.064 per minute against $0.082 per minute for the 32-core Linux larger runner (32 vCPU, 128 GB): 22 percent lower list price. The 5,355 monthly minutes come to $439.11 against $342.72.

The model holds step timings identical across every column so that only the core count and the list price move. Every cost and performance number on this page carries its source and a checked-on date; where no citable number exists, no speed claim is attached.

FAQ

What size runner should a Rust workspace start on?

warp-ubuntu-latest-x64-8x at $0.016 per minute for cargo test on a workspace whose compile set is under about 150 crates, and warp-ubuntu-latest-x64-16x at $0.032 once the set passes 150 crates or the job compiles dependencies cold. Lint and check jobs with a warm dependency cache stay on warp-ubuntu-latest-x64-4x at $0.008, because cargo check skips code generation and rarely holds four cores busy.

Does setting CARGO_BUILD_JOBS above the vCPU count help?

No. Cargo already defaults the job count to the number of logical CPUs, and oversubscribing adds rustc processes that compete for the same cores and the same memory. The reason to set the value explicitly is the opposite case: capping it below the vCPU count when a large crate or a linker peaks past the 4 GB per vCPU the Linux x64 ladder provides.

Why did doubling the runner size not halve my Rust build?

Because only the wide part of the graph scales. Every cargo build ends with a serial tail where the last workspace crates and the test binary links depend on almost everything before them, and that tail takes the same wall clock at 32 vCPU as at 8. Run cargo build --timings and read the concurrency graph: if active units sit well below your vCPU count for most of the run, the extra cores are idle and billed.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.