Skip to content
compiler.dev

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/cache to 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 --immutable fails if the lockfile would change, which is what you want in CI. Berry also has --immutable-cache to 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.

SetupCache thisInstall command
Classic 1.xyarn cache diryarn install --frozen-lockfile
Berry, global cacheyarn config get cacheFolderyarn install --immutable
Berry, zero-installnothing (committed)yarn install --immutable --immutable-cache

How to verify a cache hit

  1. The restore step log says Cache restored from key: ... on a hit and Cache not found for input keys: ... on a miss.
  2. 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.
  3. For Classic, a warm install is dominated by the linking stage; the fetching stage is short.
  4. gh cache list --key yarn- --limit 20 confirms what was saved and when it was last used.

Common mistakes

  1. Hashing the wrong file. package.json changes more often than yarn.lock and invalidates the cache needlessly. Hash the lockfile.
  2. Not enabling Corepack. setup-node's cache step runs yarn; if the wrong global Yarn answers, the cache directory it reports is wrong.
  3. One key for Classic and Berry. If you migrate, change the key prefix so the old layout is not restored.
  4. Caching on PRs only. Branch scope means PR caches are not shared. Run on main too. See Caching in GitHub Actions.
  5. 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.

Run this workflow on compiler.dev 200 free minutes

Start free