---
title: "How to set up GitHub Actions cache"
slug: "set-up-github-actions-cache"
category: "howto"
tags: ["howto", "github-actions", "ci-cd", "caching", "performance", "tooling"]
status: "stable"
last_updated: 2026-08-14
summary: "Configure GitHub Actions cache for Node.js dependencies, build outputs, and tool caches to cut CI run times by 50 to 80 percent."
related: ["[[tooling/github]]", "[[howto/migrate-from-npm-to-pnpm]]", "[[howto/audit-a-package-json-for-vulnerabilities]]", "[[ops/github-pages]]", "[[comparisons/yarn-vs-pnpm]]", "[[howto/set-up-claude-code]]", "[[comparisons/vite-vs-webpack]]"]
---

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

```yaml
- 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:

```yaml
- uses: pnpm/action-setup@v4
  with:
    version: 10

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

### 2. Cache the pnpm store explicitly for maximum speed

```yaml
- 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.

```yaml
- 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.

```yaml
- 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:

```yaml
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.

```yaml
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.

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

## Related

- [[tooling/github]]
- [[howto/migrate-from-npm-to-pnpm]]
- [[howto/audit-a-package-json-for-vulnerabilities]]
- [[ops/github-pages]]
- [[comparisons/yarn-vs-pnpm]]
- [[howto/set-up-claude-code]]
- [[comparisons/vite-vs-webpack]]
