Cache Yarn Dependencies in GitHub Actions
Last updated
To cache Yarn in GitHub Actions, set cache: yarn on actions/setup-node and install with a frozen lockfile. The catch is that Yarn Classic (1.x) and Yarn Berry (2+) store packages in different places and use different flags, so the explicit recipe differs for each.
First check which one you have: yarn --version prints 1.22.x for Classic and 3.x or 4.x for Berry.
Option 1: setup-node built-in cache
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: corepack enable # needed for Berry and packageManager pinning
- uses: actions/setup-node@v7
with:
node-version: 22
cache: yarn
- run: yarn install --immutable # Berry
# Classic: yarn install --frozen-lockfile
- run: yarn test
Per the setup-node README (checked October 2026), cache: yarn hashes yarn.lock by default, and the package manager must already be installed. corepack enable before setup-node makes the Yarn version from packageManager in package.json available. For multiple lockfiles use cache-dependency-path.
Option 2: explicit actions/cache for Classic
Classic keeps a global cache. Ask it where:
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Get Yarn cache dir
id: yarn-cache
run: echo "dir=$(yarn cache dir)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: yarn-classic-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
yarn-classic-${{ runner.os }}-
- run: yarn install --frozen-lockfile
Option 3: explicit actions/cache for Berry
Berry's cache location depends on enableGlobalCache in .yarnrc.yml. Read the actual setting instead of assuming:
- run: corepack enable
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Get Yarn cache folder
id: yarn-cache
run: echo "dir=$(yarn config get cacheFolder)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: yarn-berry-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
yarn-berry-${{ runner.os }}-
- run: yarn install --immutable
If your repo uses nodeLinker: node-modules the install still reads from this cache folder and then writes node_modules. If you use Plug'n'Play (the default), there is no node_modules; Yarn writes .pnp.cjs and reads zip archives directly.
Berry specifics worth knowing
- Zero-install. If you commit
.yarn/cacheto git, the checkout already contains the packages. There is nothing to cache; adding a cache step only adds overhead. The trade-off is a large repository. - Immutable installs.
yarn install --immutablefails if the lockfile would change, which is what you want in CI. Berry also has--immutable-cacheto fail if the cache would change. - Cache keys. Include
runner.os. Berry cache archives can hold platform-specific builds for packages with native bindings.
Why not node_modules?
Same reason as in the npm guide: a fresh install rewrites it, and native modules built for one Node version break on another. The package cache is the part that is safe to reuse and costs less to restore.
| Setup | Cache this | Install command |
|---|---|---|
| Classic 1.x | yarn cache dir | yarn install --frozen-lockfile |
| Berry, global cache | yarn config get cacheFolder | yarn install --immutable |
| Berry, zero-install | nothing (committed) | yarn install --immutable --immutable-cache |
How to verify a cache hit
- The restore step log says
Cache restored from key: ...on a hit andCache not found for input keys: ...on a miss. - For Berry, the install output prints resolution, fetch and link steps with timings. A warm cache makes the fetch step nearly instant, while a cold one lists packages being downloaded.
- For Classic, a warm install is dominated by the linking stage; the fetching stage is short.
gh cache list --key yarn- --limit 20confirms what was saved and when it was last used.
Common mistakes
- Hashing the wrong file.
package.jsonchanges more often thanyarn.lockand invalidates the cache needlessly. Hash the lockfile. - Not enabling Corepack. setup-node's cache step runs
yarn; if the wrong global Yarn answers, the cache directory it reports is wrong. - One key for Classic and Berry. If you migrate, change the key prefix so the old layout is not restored.
- Caching on PRs only. Branch scope means PR caches are not shared. Run on
maintoo. See Caching in GitHub Actions. - Ignoring size. Berry caches hold zip archives and can reach a few GB in large monorepos. Watch the 10 GB limit.
FAQ
Does setup-node support Yarn Berry caching?
Yes, with cache: yarn. Make sure Corepack has activated the right Yarn version before the setup step.
Should I commit .yarn/cache (zero-install)?
It removes the cache step and install network time, at the cost of repo size and noisy diffs. It is a team preference, not a CI requirement.
yarn install --immutable fails in CI. Why?
The lockfile on the branch is out of sync with package.json. Run yarn install locally and commit yarn.lock.
Is Yarn caching different in a monorepo?
Workspaces share one lockfile and one cache folder, so the recipe is identical. Use cache-dependency-path if you have more than one lockfile.
Next steps
See the pnpm and npm equivalents, and how to speed up GitHub Actions for what to try after caching. To find out what a better hit rate saves, use the cache savings estimator.
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.