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 shape | Crates in the compile set | Codegen units in play | Label | Price per minute |
|---|---|---|---|---|
cargo fmt, cargo clippy, cargo check with a warm dependency cache | up to 60 rebuilt | none; check skips code generation | warp-ubuntu-latest-x64-4x | $0.008 |
cargo test, dev profile, warm dependency cache | 100 to 150 | 256 per crate | warp-ubuntu-latest-x64-8x | $0.016 |
cargo test on a wide workspace, or any cold dependency compile | 150 to 600 | 256 per crate | warp-ubuntu-latest-x64-16x | $0.032 |
| Release build with thin LTO across a large graph | 600 or more | 16 per crate | warp-ubuntu-latest-x64-32x | $0.064 |
Release build with fat LTO or codegen-units = 1 | any | 1, in one final serial step | warp-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.
| Label | vCPU | Parallel phase | Serial tail | Wall clock | Cost per run | Cost per minute of wall clock removed |
|---|---|---|---|---|---|---|
warp-ubuntu-latest-x64-4x | 4 | 22.00 min | 3.20 min | 25.20 min | $0.2016 | baseline |
warp-ubuntu-latest-x64-8x | 8 | 11.00 min | 3.20 min | 14.20 min | $0.2272 | $0.0023 |
warp-ubuntu-latest-x64-16x | 16 | 5.50 min | 3.20 min | 8.70 min | $0.2784 | $0.0093 |
warp-ubuntu-latest-x64-32x | 32 | 2.75 min | 3.20 min | 5.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 16CARGO_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:
| Label | Wall clock per run | Monthly minutes | Monthly cost |
|---|---|---|---|
warp-ubuntu-latest-x64-8x | 14.20 min | 12,780 | $204.48 |
warp-ubuntu-latest-x64-16x | 8.70 min | 7,830 | $250.56 |
warp-ubuntu-latest-x64-32x | 5.95 min | 5,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.