Cache pnpm Dependencies in GitHub Actions
Last updated
To cache pnpm in GitHub Actions, install pnpm first, then set cache: pnpm on actions/setup-node. That caches pnpm's content-addressable store, keyed on pnpm-lock.yaml. The order matters: setup-node needs pnpm to exist before it runs, so pnpm/action-setup goes above it.
Option 1: setup-node cache
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test
pnpm/action-setup can omit version when package.json has a packageManager field (for example "packageManager": "pnpm@10.9.0"). That is the cleanest setup: one place defines the pnpm version for your laptop and CI. Otherwise pass with: { version: 10 }.
The setup-node README (checked October 2026) says the pnpm cache value requires pnpm 6.10 or newer and that pnpm must already be installed. If you see Unable to locate executable file: pnpm, the step order is wrong.
Option 2: let pnpm/action-setup cache the store
pnpm/action-setup has its own cache input, off by default. Its README describes it as caching the pnpm store directory, with the key from pnpm-lock.yaml (change that with cache_dependency_path).
- uses: pnpm/action-setup@v6
with:
cache: true
- uses: actions/setup-node@v7
with:
node-version: 22
- run: pnpm install --frozen-lockfile
Pick one mechanism, not both. Two caches of the same store waste quota.
Option 3: explicit actions/cache
Ask pnpm where the store is rather than guessing. The location differs by OS and pnpm version.
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Get pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- run: pnpm install --frozen-lockfile
The hashFiles('**/pnpm-lock.yaml') glob handles repos with more than one lockfile. In a standard pnpm workspace there is one lockfile at the root, so the hash is stable and changes only when dependencies do.
What to cache and what not to
pnpm keeps every package once in the store and hard-links (or copies) files into each project's node_modules. That design is why caching the store is enough.
- Cache the store. It is the expensive part: network downloads.
- Do not cache
node_modules. It is a tree of links into the store. Restored from a tarball it becomes full copies, can be huge, and may not match the store layout on the next run. - Do not hard-code the store path. Use
pnpm store path.
One caveat: hard links only work within one filesystem. If the store restores onto a different volume than your workspace, pnpm falls back to copying, which is slower but still correct.
Workspaces
For a workspace, install once at the root and run tasks with filters:
- run: pnpm install --frozen-lockfile
- run: pnpm --filter "...[origin/main]" test
For the filter to work, the checkout needs enough history. See fastest CI for pnpm workspaces for the full setup including fetch-depth.
How to verify a cache hit
- The setup-node post step logs
Cache saved with key: ...on a miss. On a hit the restore step logsCache restored from key: .... - Time
pnpm install. With a warm store, pnpm's progress line reports packages asreusedrather thandownloaded. gh cache list --key pnpm- --limit 20shows entries, sizes and last-accessed times.
A complete example with a matrix
Caches are keyed per OS, so a matrix over Node versions produces one entry per cell unless you share the store. The pnpm store is mostly independent of the Node version, so a key without the Node version is usually fine and saves quota:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [20, 22]
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test
setup-node builds its key from the OS, the package manager and the lockfile hash, so both matrix cells restore the same store entry. Packages with native add-ons are rebuilt per Node version during install, but their downloads still come from the cache. Check the cache savings estimator to see what the hit rate is worth for your run count.
Common mistakes
- Setup order.
actions/setup-nodebeforepnpm/action-setupfails forcache: pnpm. - Mismatched pnpm versions. The store layout changes between major versions. Include the pnpm major in your key if you upgrade often.
- Using
pnpm installwithout--frozen-lockfile. Be explicit in CI so a stale lockfile fails the build instead of silently resolving new versions. - Writing the cache only on pull requests. PR caches are scoped to the PR merge ref. Populate it from
mainas well. Details in Caching in GitHub Actions. - Forgetting the 10 GB cap. Each OS and Node combination makes a new entry. See cache limits.
FAQ
Where is the pnpm store on a GitHub-hosted runner?
Run pnpm store path in a step to print it. It depends on the OS and pnpm version, which is why the explicit recipe reads it dynamically.
Is pnpm faster than npm in CI?
Often on a warm store, because it links files rather than copying them. The gain depends on your tree; measure both with the same cache state.
Should I commit the pnpm store?
No. A cache is the right place for it.
What about Turborepo or Nx caches?
Those cache task outputs, not dependencies. See monorepo CI with Nx or Turborepo.
Next steps
Compare with the npm and Yarn setups. If installs are fast and the job is still slow, the bottleneck is CPU; compiler.dev runs the same workflow on 8 vCPU machines for $0.012 per minute (see pricing).
Related
- Cache Cargo and Rust Builds in GitHub ActionsCache the Cargo registry, git dependencies and target directory in GitHub Actions with Swatinem/rust-cache or actions/cache, and avoid bloated caches.
- Cache Go Modules and Build Cache in GitHub ActionsCache Go modules (GOMODCACHE) and the build cache (GOCACHE) in GitHub Actions with setup-go or actions/cache. Keys, go.sum, and verifying hits.
- Cache Gradle Builds in GitHub ActionsCache Gradle dependencies and build cache in GitHub Actions with gradle/actions/setup-gradle or actions/cache. Keys, read-only PRs, cleanup and checks.
- Cache npm Dependencies in GitHub ActionsCache npm dependencies in GitHub Actions with setup-node or actions/cache. Working YAML, the right key, why not node_modules, and how to confirm a hit.