self-host →

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:

RunnerCPU / memoryAverage runtimeCost / run
ubuntu-latest (private)2 CPU / 8 GB368.3s$0.04000
ubuntu-latest (public)4 CPU / 16 GB221.7sFree

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-latest
runs-on: runs-on=${{ github.run_id }}/family=m8azn.large/image=ubuntu24-full-x64
runs-on: ubuntu-latest
runs-on: runs-on=${{ github.run_id }}/family=m8azn.xlarge/image=ubuntu24-full-x64

We tested both on-demand and Spot instances in us-east-1, so the costs correspond to that:

RunnerAverage run timeCost/run (on-demand)Cost/run (Spot)
runs-on m8azn.large181.8s ↓51%$0.01107 ↓72%$0.00383 ↓90%
runs-on m8azn.xlarge119.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:

RunnerAverage runtimeCost / Run
ubuntu-latest private93.3s$0.01200
ubuntu-latest public66.3sFree

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-latest
runs-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:

RunnerAverage run timeCost/run (on-demand)Cost/run (Spot)
runs-on m8azn.large65.7s ↓30%$0.00470 ↓61%$0.00160 ↓87%
runs-on m8azn.xlarge55.5s ↓16%$0.00813$0.00290