Overview

GitHub Actions cache stores the results of expensive steps (dependency installs, compiled assets, tool downloads) between workflow runs. A well-configured cache cuts a 5-minute CI run to under 90 seconds on cache hits. This guide covers the actions/cache pattern, the built-in cache option in actions/setup-node, and caching for pnpm, Bun, and build outputs.

Prerequisites

  • A GitHub repository with at least one GitHub Actions workflow file (.github/workflows/*.yml).
  • A package-lock.json, pnpm-lock.yaml, or bun.lockb at the repo root.
  • Basic familiarity with YAML workflow syntax.

Steps

1. Use the built-in cache in actions/setup-node

For npm and pnpm, actions/setup-node has a built-in cache input that handles key generation and path setup automatically.

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

This caches the npm global cache (~/.npm) keyed on the lock file hash. On a cache hit, npm ci skips downloading packages from the registry.

For pnpm, add the pnpm setup action first:

- uses: pnpm/action-setup@v4
  with:
    version: 9
 
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: pnpm

2. Cache the pnpm store explicitly for maximum speed

- name: Get pnpm store directory
  id: pnpm-cache
  run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
 
- uses: actions/cache@v4
  with:
    path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-store-

restore-keys allows a partial cache hit when the lock file changes, avoiding a full cold install.

3. Cache build outputs

Cache expensive build steps when the output is deterministic given the source inputs.

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

The if condition skips the build entirely on a cache hit. Use this for compiled assets, generated types, or any artifact that is expensive to produce and deterministic given the source files.

4. Cache Playwright or Cypress browsers

Browser test runners download large browser binaries on first run.

- name: Cache Playwright binaries
  uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
 
- run: npx playwright install chromium

5. Set a cache size limit and restore order

GitHub provides 10 GB of cache per repository, evicting least-recently-used entries first. For monorepos with many packages, use scoped keys:

key: ${{ runner.os }}-${{ matrix.package }}-${{ hashFiles(format('{0}/pnpm-lock.yaml', matrix.package)) }}

Restore keys should go from most to least specific to maximize partial hit rates.

restore-keys: |
  ${{ runner.os }}-${{ matrix.package }}-
  ${{ runner.os }}-

Verify it worked

After the first workflow run that primes the cache, trigger another run. Confirm:

  1. The cache step shows “Cache hit” in the workflow log.
  2. The install step duration drops from minutes to seconds.
# Check cache usage in the GitHub API
gh api repos/{owner}/{repo}/actions/cache/usage

The active_caches_size_in_bytes field shows total cache consumption.

Common errors

  • Cache never hits: the key includes a timestamp or non-deterministic value. Keys must be reproducible from the same source files.
  • Stale cache after dependency change: if you updated package.json but not the lock file, the hash does not change. Always run the install step after cache to ensure the node_modules matches the lock file.
  • cache-hit check skips required step: confirm the if condition references the correct step ID. A typo causes the step to always skip.
  • 10 GB limit exceeded: prune old caches via the GitHub UI (Actions > Management > Caches) or the API. For large monorepos, scope cache keys more narrowly.
  • Secrets in cached artifacts: never cache directories that contain .env files, credentials, or build artifacts that embed secrets. Caches are accessible to all workflow runs on the repository.