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, orbun.lockbat 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, yarnThis 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: pnpm2. 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 buildThe 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 chromium5. 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:
- The cache step shows “Cache hit” in the workflow log.
- The install step duration drops from minutes to seconds.
# Check cache usage in the GitHub API
gh api repos/{owner}/{repo}/actions/cache/usageThe active_caches_size_in_bytes field shows total cache consumption.
Common errors
- Cache never hits: the
keyincludes a timestamp or non-deterministic value. Keys must be reproducible from the same source files. - Stale cache after dependency change: if you updated
package.jsonbut not the lock file, the hash does not change. Always run the install step after cache to ensure thenode_modulesmatches the lock file. cache-hitcheck skips required step: confirm theifcondition 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
.envfiles, credentials, or build artifacts that embed secrets. Caches are accessible to all workflow runs on the repository.