Best practice · Caching & Setup

Cache dependencies in GitHub Actions

Cache your package manager's downloads so CI restores dependencies from a keyed cache instead of reinstalling them from scratch on every run.

Do this

The setup action (or an explicit actions/cache step) restores the dependency store keyed on the lockfile. When the lockfile is unchanged, the install is a fast cache restore instead of a full network download and rebuild. Most ecosystems have a first-class hook: cache: on actions/setup-node (npm, pnpm, yarn), actions/setup-python (pip, pipenv, poetry), actions/setup-go (the module and build caches), and actions/setup-java (maven, gradle). Set that input explicitly rather than relying on a default: actions/setup-node v5 began auto-caching when it detected a package manager, and v6 narrowed that to npm only (and only when package.json names npm in packageManager or devEngines.packageManager), so a pnpm or yarn repo that never sets cache: gets no cache at all. Cargo has no built-in hook, so key an actions/cache step on Cargo.lock yourself (or use Swatinem/rust-cache, which does it for you).1234

Cache keyed on the lockfile, per ecosystem
# Take the block for your ecosystem. Each one keys the cache on the lockfile,
# so an unchanged lockfile restores instead of reinstalling.
steps:
  - uses: actions/checkout@v7

  # Node (npm / pnpm / yarn): setup-node caches the package manager's store.
  # From setup-node v6, only npm is cached automatically (and only when
  # package.json names npm in "packageManager"). pnpm and yarn still need
  # this explicit cache: input.
  - uses: pnpm/action-setup@v6
    with:
      version: 10   # required unless package.json sets "packageManager"
  - uses: actions/setup-node@v6
    with:
      node-version: 22
      cache: pnpm                  # keyed on pnpm-lock.yaml
  - run: pnpm install --frozen-lockfile

  # Python: setup-python caches the pip download cache
  - uses: actions/setup-python@v6
    with:
      python-version: "3.13"
      cache: pip                   # keyed on requirements.txt

  # Go: setup-go caches the module + build cache, keyed on go.sum
  - uses: actions/setup-go@v6
    with:
      go-version: "1.24"
      cache: true

  # Java: setup-java caches the Gradle or Maven dependency store
  - uses: actions/setup-java@v5
    with:
      distribution: temurin
      java-version: "21"
      cache: gradle                # or: maven

  # Rust: no built-in hook. Cache the resolved registry and the build dir, keyed on
  # Cargo.lock. Prefer Swatinem/rust-cache, which does all of this and prunes; if you
  # roll it by hand, copy actions/cache's own Rust example and note the two traps.
  - uses: actions/cache@v6
    with:
      # Not the whole ~/.cargo/registry: registry/src and git/checkouts are unpacked
      # from the cached archives, so caching them stores the same bytes twice.
      path: |
        ~/.cargo/bin/
        ~/.cargo/registry/index/
        ~/.cargo/registry/cache/
        ~/.cargo/git/db/
        target/
      key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
      # NO restore-keys here, deliberately. A prefix fallback restores a target/ built
      # against a DIFFERENT Cargo.lock, then saves it again under the new key: the stale
      # artifacts never get evicted and the cache grows without bound. A cache miss on a
      # dependency change is the correct outcome.

Avoid this

Every run pays the full install cost, and it multiplies across every matrix leg and shard.

No cache: full install every run
steps:
  - uses: actions/checkout@v7
  - uses: pnpm/action-setup@v6
    with:
      version: 10   # required unless package.json sets "packageManager"
  - uses: actions/setup-node@v6
    with:
      node-version: 22       # no cache: key, and setup-node never auto-caches
                             # pnpm, so nothing is restored
  - run: pnpm install        # re-downloads every dependency, every run

Seen in the wild

Real workflow files from public projects.

              with:
                  node-version: "22"
                  cache: "npm"
                  cache-dependency-path: backend/package-lock.json
A monorepo wrinkle the clean example above leaves out: cache-dependency-path tells setup-node which lockfile to key the cache on.
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'
The same single line in Python, on the job that fans setup-python out across every supported version.
      - name: Setup Node
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version-file: '.nvmrc'
          cache: 'pnpm'
The pnpm store, restored by setup-node and keyed on the lockfile, on the test job of a self-hosted photo app.

Why it matters

Reinstalling dependencies from scratch wastes minutes on every job, and the waste multiplies across every matrix leg and shard. The one caveat to size honestly: caching only helps runs whose lockfile is unchanged (most PRs, re-runs, and dependabot bumps aside), so measure your real cache-hit rate rather than assuming every run benefits. Building Docker images in the same pipeline? Layer caching is the container-side analog, covered in Docker builds in GitHub Actions.

When to use

Use it when

Any job that installs dependencies with a lockfile, which is almost every build and test job.

Be careful when

Skip a cache only when installs are already trivial (a handful of packages) or when the cache key would change on nearly every run, so the restore never hits.

Verify on your repo

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to audit and fix this practice in your own repo.

Prompt for your coding agent
Inspect this repo's .github/workflows for dependency caching. For every job that installs packages (npm, pnpm, yarn, pip, cargo, go, gradle, maven), check whether a cache is configured: a `cache:` key on `actions/setup-node` / `setup-python` / `setup-go`, an explicit `actions/cache` step, or an ecosystem cache like `Swatinem/rust-cache`. Flag any install step (`pnpm install`, `pip install`, `cargo build`, etc.) that has no matching cache restoring its store, and confirm the cache key is derived from the lockfile so it actually hits. Add the right cache hook keyed on the lockfile, then show me the diff and open a PR rather than applying it blindly.

Ground these changes in the upstream docs before you edit: https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching, https://github.com/actions/cache. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.

Prefer to check by hand?

  1. Search your workflows for a cache hook: grep -rn 'cache' .github/workflows/, look for cache: on a setup action, actions/cache, or an ecosystem cache like Swatinem/rust-cache.

  2. Confirm the install command is present but no matching cache exists (e.g. pnpm install / pip install / cargo build with nothing restoring its store).

  3. Read a job log: a warm run should show a cache-restore line and a much shorter install step than a cold run.

Automate this with StarSling.

Rather than audit this by hand, install the StarSling GitHub App: it opens a pull request to move your workflows onto StarSling runners, then its agents keep reading your jobs, run logs, and machine telemetry and open pull requests that fix what this page covers, along with caching, parallelization, and test sharding across the rest of your pipeline. Every change arrives as a PR you review and merge.

More best practices for GitHub Actions

Where to go next in the CI best-practices catalog.

All CI best practices

FAQ

Does actions/cache or the setup action's cache: option work better?

For the common ecosystems, the built-in cache: on actions/setup-node, setup-python, and setup-go is simplest, it picks a sensible key from your lockfile automatically. Reach for an explicit actions/cache step when you need to cache something the setup action doesn't cover, like a build cache or a browser-binary download.

Why is my cache not speeding anything up?

Usually the cache key changes too often (so every run is a miss), or the cached path isn't where the tool actually reads from. Check the restore/miss lines in the job log and confirm the key is derived from the lockfile, not from something that changes each run.

How do I cache pnpm in a monorepo or workspace?

Point actions/setup-node at the lockfiles with cache-dependency-path: '**/pnpm-lock.yaml' (a multi-line list of explicit paths works too). On its own, cache: only looks for that package manager's lockfile in the working-directory root, so in a workspace whose lockfiles live in sub-projects the step does not quietly mis-key the cache: it fails the job outright with Dependencies lock file is not found. cache-dependency-path is what points it at the lockfiles you actually have, and makes the key cover every one of them.

Sources

1GitHub Docs · caching dependencies to speed up workflows (opens in new tab)

2actions/setup-node · built-in dependency caching (opens in new tab)

3actions/cache (opens in new tab)

4Swatinem/rust-cache (opens in new tab)

Last updated 2026-07-15

Get started

Cache dependencies once. Reuse them every run.

One line to install. Faster runs on day one, and agents that open reviewable PRs to keep your pipeline following practices like this one.