Making Rust build faster on runs-on
We compared a Rust project's CI performacnce on matched 2-CPU and 4-CPU GitHub and runs-on runners, with and without caching.
After seeing Namespace speeding up a Rust repo ↗ we thought we’d give it a go as well!
The workflow runs on GitHub-hosted ubuntu-latest machines and since the spotify-player repo is public, the machines are on the public spec: 4CPU-16GB, as opposed to the private spec: 2CPU-8GB.
GitHub also doesn’t charge for Actions runs in public repos (for standard instances anyway).
So we ran our benchmarks on both specs, thrice for each config to get an average, and compared them to the performance and cost of their equivalent runs-on runners, with and without caching.
GitHub baseline#
The workflow installs Ubuntu libraries and Rust, then runs formatting checks, tests and Clippy.
The cold ubuntu-latest runs looked like this:
| Runner | CPU / memory | Average runtime | Cost / run |
|---|---|---|---|
ubuntu-latest (private) | 2 CPU / 8 GB | 368.3s | $0.04000 |
ubuntu-latest (public) | 4 CPU / 16 GB | 221.7s | Free |
Replace with RunsOn runners#
Next, we switched to m8azn.large for the 2-CPU jobs and m8azn.xlarge for the 4-CPU jobs. Both match the CPU and memory allocation of their GitHub counterparts:
runs-on: ubuntu-latestruns-on: runs-on=${{ github.run_id }}/family=m8azn.large/image=ubuntu24-full-x64runs-on: ubuntu-latestruns-on: runs-on=${{ github.run_id }}/family=m8azn.xlarge/image=ubuntu24-full-x64We tested both on-demand and Spot instances in us-east-1, so the costs correspond to that:
| Runner | Average run time | Cost/run (on-demand) | Cost/run (Spot) |
|---|---|---|---|
runs-on m8azn.large | 181.8s ↓51% | $0.01107 ↓72% | $0.00383 ↓90% |
runs-on m8azn.xlarge | 119.2s ↓46% | $0.01557 | $0.00540 |
Changing the runner cut runtime by 49–52% at 2 CPUs and 46% at 4 CPUs.
Adding GitHub cache#
The Namespace article missed comparing GitHub cache with their own cache solution (they’d have won still). But we’ll go ahead and do that.
So just like original workflow, we cached Cargo dependencies and compiled build files in target/.
But to emulate real-world cache scenarios, we applied a small change and added a regression test before every measurement, so the cached jobs still had to rebuild the changed application.
This made cache measurements more realistic:
| Runner | Average runtime | Cost / Run |
|---|---|---|
ubuntu-latest private | 93.3s | $0.01200 |
ubuntu-latest public | 66.3s | Free |
Caching alone cut GitHub’s runtime by 75% at 2 CPUs and 70% at 4 CPUs.
Replace with runs-on magic cache#
We cached the same Rust directories with RunsOn’s Magic Cache ↗.
runs-on: ubuntu-latestruns-on: runs-on=${{ github.run_id }}/family=m8azn.large/image=ubuntu24-full-x64/extras=s3-cache
steps: - uses: actions/checkout@v4 - uses: runs-on/action@v2
- uses: actions/cache@v4 with: path: | ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: rust-${{ runner.os }}-1.98.1-${{ hashFiles('**/Cargo.lock') }}Performance wise the Github cache is not behind, but the costs is where runs-on runners take the win:
| Runner | Average run time | Cost/run (on-demand) | Cost/run (Spot) |
|---|---|---|---|
runs-on m8azn.large | 65.7s ↓30% | $0.00470 ↓61% | $0.00160 ↓87% |
runs-on m8azn.xlarge | 55.5s ↓16% | $0.00813 | $0.00290 |