Cache npm Dependencies in GitHub Actions
Cache the npm store between GitHub Actions runs to cut install time from minutes to seconds. Lockfile-aware cache key included.
GitHub Actions caching for npm dependencies turns a 60-second npm install into a 5-second cache restore. The simplest pattern is to use actions/setup-node's built-in cache option, which hashes your lockfile and stores the npm package store. For more control, use actions/cache directly.
Tested on GitHub Actions ubuntu-latest, Node 24, npm 11.
When to Use This
- Every workflow that runs
npm ciornpm install - Monorepos with multiple lockfiles (each gets its own cache)
- Workflows that run on every PR push (cache hits dominate)
- Self-hosted runners where you want persistent local caching
Don't use this when the install is already fast enough that the cache restore would be slower than a fresh install (rare — usually only for tiny projects).
Code
The simplest version — actions/setup-node handles everything:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm' # or 'pnpm', 'yarn'
- name: Install dependencies
run: npm ciThe cache: 'npm' option:
- Hashes your
package-lock.jsonto create a cache key - Restores the npm cache from previous runs if the key matches
- Saves the cache automatically at the end of the job
For monorepos with multiple lockfiles, point at the specific one:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: |
apps/web/package-lock.json
packages/ui/package-lock.jsonUsage
When you need finer control (uncommon, but useful), use actions/cache directly:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
- name: Get npm cache directory
id: npm-cache
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- name: Restore npm cache
uses: actions/cache@v4
with:
path: ${{ steps.npm-cache.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: npm run buildThe restore-keys block provides a fallback: if the exact lockfile hash isn't cached, fall back to any cache from the same OS. This gives you a partial speedup when the lockfile changes by only a few packages.
Pitfalls
npm ciis required, notnpm install.cienforces the lockfile and is faster.installmutates the lockfile, which breaks reproducibility.- Don't cache
node_modulesdirectly. It contains compiled binaries that may not work across Node versions or runners. Cache the npm store instead and re-runnpm ci. - Cache size limits matter. GitHub Actions caches are capped at 10 GB per repo (5 GB on free plans for old caches). Old entries are evicted automatically by least-recently-used.
hashFilespaths are relative to the repo root. Globs work:**/package-lock.jsoncatches every lockfile in the repo.- Cache hits are not retroactive. You only see cache hits after the first run that creates the cache. The first PR after adding caching is slow.
actions/setup-node@v4cache only saves on default branch by default. This prevents PR caches from polluting the main cache. Override withsave-always: trueif you want PR caches saved too.
Related Snippets & Reading
- Next.js CI Workflow — uses this caching pattern
- Matrix Build Across Node Versions — caching works per-matrix-job
- actions/setup-node caching docs — official caching reference
Frequently Asked Questions
Should you cache node_modules or the npm cache?
Cache the npm cache (the global package store), not node_modules. node_modules is platform-specific, contains binary builds, and gets stale fast. The npm store is platform-shared, content-addressed, and always reproducible by re-running npm ci. actions/setup-node uses this approach by default.
What's the right cache key for npm dependencies?
Hash the lockfile (package-lock.json or pnpm-lock.yaml). When the lockfile changes, the cache key changes, and you get a fresh cache. When the lockfile is unchanged, you get a cache hit. actions/setup-node does this automatically when you set the cache option.