Overview

A cache restores the result of a slow step, such as a dependency download or an image build, when its inputs have not changed. This guide uses the built-in caching in actions/setup-node for packages and actions/cache for build outputs and browsers. The key rule: derive the cache key from a hash of the inputs, never from a timestamp.

Prerequisites

  • A workflow in .github/workflows/ and a committed lockfile (package-lock.json, pnpm-lock.yaml, or bun.lock).

Steps

1. Cache dependencies through setup-node

- uses: actions/setup-node@v7
  with:
    node-version: 22
    cache: npm          # or pnpm, yarn
- run: npm ci

This caches the package manager’s download cache (for npm, ~/.npm), keyed on the lockfile hash. npm ci still runs and still deletes node_modules, so only the downloads are skipped. For pnpm, install pnpm before setup-node so the cache step can find it:

- uses: pnpm/action-setup@v6      # version from "packageManager" (pnpm 11+); else set `version:`
- uses: actions/setup-node@v7
  with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile

Pin pnpm with the packageManager field in package.json so CI and local use the same version. The pnpm docs now also show a pnpm/setup action that installs pnpm and Node together.

2. Cache build outputs

Cache only deterministic outputs. Skip the build step when the exact key hits.

- uses: actions/cache@v6
  id: build-cache
  with:
    path: dist
    key: ${{ runner.os }}-build-${{ hashFiles('src/**', 'vite.config.ts') }}
- if: steps.build-cache.outputs.cache-hit != 'true'
  run: npm run build

3. Cache browser binaries

- uses: actions/cache@v6
  with:
    path: ~/.cache/ms-playwright
    key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
- run: npx playwright install --with-deps chromium

Keep the install step: it is fast on a hit and necessary on a miss, and --with-deps installs system libraries that the cache does not hold.

4. Use restore-keys for partial hits

key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
  ${{ runner.os }}-pnpm-

When the exact key misses, GitHub tries each restore-keys prefix in order and restores the most recent match, so a lockfile change reuses most of the old cache. List keys from most to least specific.

Verify it worked

Run the workflow twice on the same branch. The second run’s cache step logs “Cache restored” and the install step takes seconds instead of minutes.

gh api repos/{owner}/{repo}/actions/cache/usage

Common errors

  • The cache never hits. The key contains a timestamp, run ID, or other changing value.
  • Stale node_modules. Caching node_modules directly skips the lockfile check; cache the package-manager download cache and still run the install command.
  • A feature branch misses. Caches are readable from the same branch and the default branch, and pull requests also read the base branch’s; a cache from a sibling feature branch is invisible. Run the workflow once on main.
  • Eviction. The default limit is 10 GB per repository, and entries unused for 7 days are removed, oldest first. Narrow keys or delete old caches under Actions, Caches.
  • Secrets in a cache. Anyone who can open a pull request can read cached files; never cache .env files or credentials.

FAQ

Should I cache node_modules in GitHub Actions?

No; caching node_modules directly skips the lockfile check. Cache the package manager’s download cache, for example with setup-node’s cache: npm, and still run the install command.

Why does my GitHub Actions cache never hit?

The key contains a timestamp, run ID, or other changing value; derive it from a hash of the inputs, such as hashFiles('package-lock.json').

Why can’t my feature branch read a cache from another branch?

Caches are readable from the same branch and the default branch, and pull requests also read the base branch’s. Run the workflow once on main so sibling branches can restore it.

How big can the GitHub Actions cache get?

The default limit is 10 GB per repository, and entries unused for 7 days are removed, oldest first.

Is it safe to cache .env files in GitHub Actions?

No; anyone who can open a pull request can read cached files, so never cache .env files or credentials.