Cache npm Dependencies in GitHub Actions
Last updated
To cache npm dependencies in GitHub Actions, add cache: npm to actions/setup-node and install with npm ci. That caches npm's download directory (~/.npm), keyed on your package-lock.json. If you need control over the key or path, the explicit actions/cache form below does the same thing.
This page is the npm-specific recipe. For how keys, scope and restore-keys work in general, read Caching in GitHub Actions first.
Option 1: setup-node built-in cache
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
The action hashes package-lock.json (or npm-shrinkwrap.json) at the repo root to build the key. In a monorepo or when the lockfile is not at the root, point it at the right file:
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: |
package-lock.json
apps/web/package-lock.json
cache-dependency-path only has an effect when cache is set. Per the setup-node README (checked October 2026), npm caching can also switch on automatically when package.json declares npm in the packageManager field, and you can turn that off with package-manager-cache: false. Setting cache: npm explicitly keeps behaviour obvious.
Option 2: explicit actions/cache
Use this when you want a custom key, a separate restore and save, or you are not using setup-node.
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Get npm cache directory
id: npm-cache
run: echo "dir=$(npm config get cache)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6
with:
path: ${{ steps.npm-cache.outputs.dir }}
key: npm-${{ runner.os }}-node22-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-node22-
- run: npm ci
Asking npm for its cache directory avoids hard-coding ~/.npm, which is wrong on Windows runners. The key has the OS, the Node major version and the lockfile hash. The restore-keys line means a one-package change still restores most of yesterday's cache, so npm ci only downloads the difference.
Cache ~/.npm, not node_modules
The common advice to cache node_modules backfires with npm. npm ci deletes node_modules before it installs, so a restored copy is thrown away. You pay to download and extract it, then pay again to rebuild it.
Caching ~/.npm is different. npm ci still unpacks every package, but it reads tarballs from local disk instead of the registry. That is where the network time goes.
| Cache | Works with npm ci | Notes |
|---|---|---|
~/.npm | Yes | Small, safe across Node versions |
node_modules | No | Deleted on install; native modules break across Node versions |
.next/cache, .vite, .eslintcache | Yes, separately | Build and lint caches, not dependencies |
If install time is still a problem after caching, the unpack step is CPU and disk bound. That is a machine problem, not a cache problem.
How to verify a cache hit
- Open the setup-node or cache step in the job log. A hit prints
Cache restored from key: .... A miss printsCache not found for input keys: .... - With
actions/cache, thecache-hitoutput is'true'only on an exact key match. Arestore-keyspartial match gives'false'even though files were restored. - At the end of a miss, look for
Cache saved with key: ...in the post step. - List what exists with the CLI:
gh cache list --key npm- --limit 20
Run the workflow twice on the same lockfile. The second run should say restored, and the install step should drop noticeably.
Common mistakes
- Using
npm installinstead ofnpm ci.npm installcan rewrite the lockfile, so your key and your installed tree drift apart. - A lockfile that is not committed.
hashFilesreturns an empty string, every run uses the same key, and the cache never updates. setup-node fails with an error in this case, which is useful. - Different Node majors sharing a key. Add the version to the key if you build a matrix.
- Caches created only on pull requests. PR caches are scoped to the merge ref and are not visible to other PRs. Make sure pushes to
mainrun the same cache step so each PR can read a warm cache. - Postinstall binaries. Packages such as Playwright, Cypress and Puppeteer download browsers outside
~/.npm. Cache those directories separately; see Playwright tests slow and Cypress tests slow.
Size and eviction
A typical npm cache is a few hundred MB. The repository limit is 10 GB, and old entries are evicted least recently used first, with anything unused for 7 days removed. A matrix over several Node versions and operating systems multiplies entries quickly. See cache limits and eviction for how to see what is using your quota.
The cache hit savings estimator shows what a better hit rate is worth in minutes.
FAQ
Should I use npm ci or npm install in CI?
Use npm ci. It installs exactly what the lockfile says, fails if package.json and the lockfile disagree, and is faster for clean installs.
Does the setup-node cache work on pull requests from forks?
It restores caches the base branch created. Saves from fork PRs are limited by GitHub's token permissions, so rely on main to populate the cache.
Why does my npm cache restore but npm ci is still slow?
The cache removes downloads, not extraction and lifecycle scripts. Look at the time spent in postinstall scripts, or try a faster runner.
Can I cache npm across different workflows?
Yes. Caches belong to the repository. Same key and path, same cache.
Next steps
If you also use pnpm or Yarn, see pnpm caching and Yarn caching. For the full list of fixes, read how to speed up GitHub Actions. compiler.dev runs your jobs on 8 vCPU machines with a dependency cache ready; 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 pip Dependencies in GitHub ActionsCache pip in GitHub Actions with setup-python or actions/cache, plus the faster uv option. Keys, what not to cache, and how to verify a hit.