---
title: "Cache dependencies in GitHub Actions, the right way"
description: "Stop reinstalling packages on every CI run. How to cache npm, pnpm, pip, cargo, Go, and Gradle dependencies in GitHub Actions, with copyable YAML."
url: https://starsling.dev/best-practices/github-actions/cache-dependencies
canonicalUrl: https://starsling.dev/best-practices/github-actions/cache-dependencies
---

# Cache dependencies in GitHub Actions

Best practice: Caching & Setup. Last updated: 2026-07-06

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. Each ecosystem has a first-class hook: `cache:` on `actions/setup-node` / `setup-python` / `setup-go`, `Swatinem/rust-cache` for Cargo, or `actions/cache` on `~/.m2` and `~/.gradle`.

_Cache keyed on the lockfile_

```yaml
steps:
  - uses: actions/checkout@v4
  - uses: pnpm/action-setup@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 22
      cache: pnpm            # caches the pnpm store, keyed on pnpm-lock.yaml
  - run: pnpm install --frozen-lockfile
```

## Avoid this

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

_No cache: full install every run_

```yaml
steps:
  - uses: actions/checkout@v4
  - uses: pnpm/action-setup@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 22       # no cache: key, so nothing is restored
  - run: pnpm install        # re-downloads every dependency, every run
```

## 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.

## 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 to audit and fix this practice in your own repo:

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.

Prefer to check by hand:

- 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`.
- Confirm the install command is present but no matching cache exists (e.g. `pnpm install` / `pip install` / `cargo build` with nothing restoring its store).
- Read a job log: a warm run should show a cache-restore line and a much shorter install step than a cold run.

## 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.

## More best practices for GitHub Actions

- [Use a shallow checkout in GitHub Actions](https://starsling.dev/best-practices/github-actions/shallow-checkout)
- [Shard tests across parallel jobs in GitHub Actions](https://starsling.dev/best-practices/github-actions/shard-tests)
- [All CI best practices](https://starsling.dev/best-practices/github-actions)

## Sources

1. [actions/setup-node - built-in dependency caching](https://github.com/actions/setup-node)
2. [actions/cache](https://github.com/actions/cache)
3. [Swatinem/rust-cache](https://github.com/Swatinem/rust-cache)
