DEV Community

Ptagl
Ptagl

Posted on • Originally published at ptagl.github.io on

How to use the cache with GitHub Actions - The right way

Introduction

I’ve been using GitHub Actions heavily for CI on my Rust projects lately. I’m always obsessed with increasing execution speed and efficiency, and recently, I thought I had found a clever optimization that turned out to be a huge mistake.

Here is the context:

  • Development strategy: I keep a main branch on which I merge changes through feature branches and pull requests.
  • CI workflow: I usually run a standard lint (cargo clippy and cargo fmt), build, and test pipeline. My CI was configured to run on every push to a pull Request. Since I strictly merge PRs via rebase onto main, the CI run on the PR and the hypothetical run on main after the merge are mathematically identical.

So, to save on GitHub Action minutes, I had a "brilliant" idea: I disabled the CI triggers on the main branch.

I thought I was being efficient. In reality, I broke my caching strategy completely.

The Mortal Sin: Ignoring Scope

The moment I disabled the build on main, my cache hit rate immediately dropped to zero. I noticed that every new PR was compiling from scratch, only hitting the cache on subsequent commits within that PR.

The reason lies in how GitHub Actions scopes its cache:

  1. Main Branch: When CI runs on main, the cache it saves is accessible to main and any child branch (future PRs).

  2. PR Branch: When CI runs on a PR branch, the cache is saved and scoped only to that specific branch.

Once a PR is merged, that branch is deleted. If you don't run CI on main to regenerate/update the "central" cache, that data is lost to the void. New PRs cannot access the cache generated by previous PRs; they can only access the cache from the base branch (main).

By disabling CI on main, I ensured that every new PR started with a cold cache.

How to check if you are doing it wrong

You don't need complex metrics. Just look at your CI logs.

If you are using Swatinem/rust-cache or sccache, look at the setup/restore steps.

  • Good: The logs explicitly state a cache entry was found and restored.
Run Swatinem/rust-cache@v2
Cache Configuration

... Restoring cache ...
Cache hit for restore-key: v0-rust-build-and-test-Linux-x64-xxxxxxxx-xxxxxxxx
Enter fullscreen mode Exit fullscreen mode
  • Bad: The logs say "No cache found," and it begins downloading the crates registry or compiling dependencies from zero.
Run Swatinem/rust-cache@v2
Cache Configuration

... Restoring cache ...
No cache found.
Enter fullscreen mode Exit fullscreen mode

The Right Workflow

For my Rust setup, I use Swatinem/rust-cache (which is excellent for standard cargo registry/target caching) and for heavier projects, I add sccache to cache the compilation artifacts.

Here is the corrected workflow. It runs both on PRs and main, preventing the "cold start" problem.

name: Rust CI

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

env:
  CARGO_TERM_COLOR: always
  # [Optional] Enable sccache for rustc (not needed if you don't want to use `sccache`)
  RUSTC_WRAPPER: sccache
  # [Optiona] Tell sccache to use GitHub Actions cache API (not needed if you don't want to use `sccache`)
  SCCACHE_GHA_ENABLED: "true"

jobs:
  test:
    name: Test & Lint
    runs-on: ubuntu-latest
    steps:
      # 1. Checkout
      - name: Git checkout
        uses: actions/checkout@v5

      # 2. Setup the toolchain
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          components: rustfmt, clippy

      # 3. Setup Cargo cache (registry + target)
      - name: Rust Cache
        uses: Swatinem/rust-cache@v2

      # 4. [Optional] Setup sccache (mainly useful for heavy projects)
      - name: Run sccache-cache
        uses: mozilla-actions/sccache-action@v0.0.9

      # 5. Check formatting
      - name: Cargo fmt
        run: cargo fmt --all -- --check

      # 6. Linting
      - name: Cargo clippy
        run: cargo clippy --all-targets -- -D warnings

      # 7. Build
      - name: Cargo build
        run: cargo build

      # 8. Test
      - name: Cargo test
        run: cargo test
Enter fullscreen mode Exit fullscreen mode

References

Top comments (0)