How to cache dependencies in GitHub Actions (npm, pnpm, yarn)
Stop re-downloading node_modules on every run: the one-line setup-node cache, pnpm and yarn setups, actions/cache for everything else, and when not to cache.
Without a cache, every CI run downloads your whole dependency tree from the registry again. On a typical JavaScript project that is tens of seconds to a few minutes per job, on every push. In our scan of popular JS/TS repos, 41 of 121 (33.9%) had at least one CI workflow installing dependencies with no cache.
npm: one line in setup-node
actions/setup-node has a built-in cache. Add cache: npm and it saves and restores the npm cache directory, keyed on package-lock.json:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: lts/*
cache: npm
- run: npm ciThis caches the download cache (~/.npm), not node_modules. That is deliberate: npm ci still does a clean install, it just reads packages from disk instead of the network.
Since setup-node v5, npm projects get this cache automatically when package.json has a packageManager field set to npm (for example "packageManager": "npm@11.6.0"). If you rely on that, there is nothing to add. To turn it off on purpose, set package-manager-cache: false.
pnpm
pnpm has to be installed before setup-node runs, otherwise setup-node can't find the pnpm store to cache:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6 # reads the version from "packageManager"
- uses: actions/setup-node@v7
with:
node-version: lts/*
cache: pnpm
- run: pnpm install --frozen-lockfileYarn
- uses: actions/checkout@v7
- run: corepack enable # Yarn 2+ (Berry); skip for Yarn 1
- uses: actions/setup-node@v7
with:
node-version: lts/*
cache: yarn
- run: yarn install --immutable # Yarn 1: yarn install --frozen-lockfileAnything else: actions/cache
For build outputs and tool caches (Next.js .next/cache, Turborepo, Playwright browsers), use actions/cache with a key that changes when the inputs change:
- uses: actions/cache@v6
with:
path: .next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**') }}
restore-keys: |
nextjs-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-restore-keys lets a run start from the closest older cache when there is no exact match, which is what makes build caches useful at all.
How the cache behaves
- A run can restore caches from its own branch and from the default branch. A pull request can read from main, but a cache saved in a PR is not visible to other branches.
- Caches not accessed for 7 days are deleted, and the repository has a total size limit (10 GB by default). Oldest caches are evicted first.
- A cache is saved only when the job succeeds, and an existing key is never overwritten. Put the lockfile hash in the key so a dependency change creates a new entry.
When not to cache
Release and publish workflows are a reasonable exception. A poisoned cache can end up inside the artifact you ship, so many projects install from scratch there on purpose. Our scanner does not flag release, publish or deploy workflows for this reason.