Back to monorepo orchestration Target affected workspaces Configure turbo pipelines Speed up type-checking

Fixing pnpm Copying Packages Instead of Hard-Linking

pnpm's speed and disk efficiency come from hard links: every file in node_modules is a link to a single copy in the content-addressable store, so installing the same package in ten projects costs the disk space of one. When pnpm cannot hard-link — because the store and the project are on different filesystems, or the filesystem does not support it — it silently falls back to copying. Installs get slower, node_modules balloons, and nothing tells you why unless you look. This guide explains how pnpm chooses between cloning, hard-linking and copying, how to detect a fallback, and how to put the store where linking works.

Symptoms

The main symptom is slowness and disk usage, with a warning that is easy to miss:

$ pnpm install
 WARN  Falling back to copying packages from store, as it is not possible to hard link from /home/dev/.local/share/pnpm/store/v10 to /mnt/projects/acme/node_modules
Packages: +1843
Progress: resolved 1843, reused 1843, downloaded 0, added 1843, done
Done in 1m 42s

The same install on a correctly configured machine takes a few seconds. Disk usage confirms it:

du -sh node_modules
# 1.9G  node_modules     (copies)
# versus roughly the size of the unique files when hard-linked, shared with other projects

Common environments where this happens: projects on a second disk or mounted volume, WSL projects under /mnt/c with the store in the Linux filesystem, Docker volumes, network drives, and CI runners where the cache directory is on a different mount from the workspace.

Root cause analysis

A hard link is a second directory entry for the same file on the same filesystem; it cannot cross filesystem boundaries. pnpm's store must therefore be on the same filesystem (the same mount, in practice) as node_modules for linking to work. The link strategy is described in Workspace Symlinks vs Hard Links.

How pnpm imports files from the store pnpm tries copy-on-write cloning first, then hard links, then falls back to plain copies when neither is possible across the store and project filesystems. Does the filesystem support reflinks (clone)? Clone copy-on-write: fast, isolated, no extra space yes Are store and project on one filesystem? Hard link shared inode: fast, no extra space yes no Copy full copies: slow and disk-hungry no
With package-import-method auto, pnpm silently degrades to copying when the store is on another filesystem.

The package-import-method setting controls the choice. The default, auto, tries cloning (copy-on-write reflinks on filesystems such as Btrfs, XFS, APFS and ReFS), then hard-linking, then copying. When pnpm detects that the store is on a different filesystem from the project, it can also create a new store on the project's filesystem — but only if the default store location is in use and pnpm can write there. With a custom store-dir on another disk, or a read-only location, it copies instead.

Resolution

Getting links back Depending on the environment, move the store onto the project's filesystem, move the project, or accept copying where linking is impossible. Store and project on different mounts check with df Store on that disk store-dir=/mnt/projects/.pnpm-sto re second disk Project in Linux FS move repo out of /mnt/c WSL Same volume for both cache the store on the workspace mount Docker / CI
The fix is almost always to put the store and the project on the same mount.

1. Find out where things are.

pnpm store path
df -h "$(pnpm store path)" .

If the two df lines show different filesystems, linking cannot work.

2. Put the store on the project's filesystem. For a projects disk, set a store directory on that disk once, for all projects there:

# ~/.npmrc (user-level), or the project .npmrc for one repo
store-dir=/mnt/projects/.pnpm-store

Then reinstall so existing copies are replaced by links:

rm -rf node_modules && pnpm install

3. On WSL, keep projects in the Linux filesystem. Projects under /mnt/c/... live on the Windows filesystem, accessed through a translation layer that is slow and does not support hard links from the Linux-side store. Moving the repository to ~/src/acme inside WSL fixes both speed and linking.

4. In Docker and CI, make the store part of the same volume as the workspace, or accept copying in the environment and cache the store instead of node_modules. For example, in a Dockerfile, use a cache mount at a path on the same filesystem as the build directory:

RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
    pnpm config set store-dir /pnpm/store && \
    pnpm install --frozen-lockfile

BuildKit cache mounts and the build directory share the container's filesystem, so linking works during the build. Remember that files in the final image must be real files, not links into a cache mount that disappears — pnpm install inside the image is fine because the links are on the image's filesystem, but copying node_modules between stages copies the linked files as normal files, which is what you want.

Choosing an import method explicitly

You can make the behaviour explicit instead of relying on auto:

# Fail fast instead of silently copying (useful on developer machines)
package-import-method=hardlink

# Or prefer copy-on-write clones where available, falling back to copy
package-import-method=clone-or-copy

With hardlink, an install that cannot link fails with an error, which turns a silent performance problem into a visible configuration problem. clone-or-copy is a good choice on filesystems that support reflinks: clones behave like independent copies (editing a file in node_modules does not change the store) but cost no extra space until modified.

Install time for the same lockfile by import method Example warm-store install times for a 1,800-package workspace using clone, hard link and copy. clone (reflink) 4 s hard link 5 s copy (different filesystem) 102 s
Linking or cloning makes warm installs near-instant; copying scales with the size of node_modules.

Verifying that files are really linked

The warning is not the only evidence; you can inspect the files directly. A hard-linked file has a link count greater than one, and its inode number matches the corresponding file in the store:

# Link count (second column) above 1 means the file is hard-linked
ls -l node_modules/.pnpm/react@18.3.1/node_modules/react/index.js

# Compare inode numbers with stat
stat -c '%i %h %n' node_modules/.pnpm/react@18.3.1/node_modules/react/index.js

A link count of 1 on a file that pnpm installed means it was copied (or cloned — clones also show a count of 1, because they are separate inodes that share blocks). On filesystems that support reflinks, filefrag -v or filesystem-specific tools can confirm shared extents, but in practice install speed is the clearer signal: a warm clone or link install of a large workspace takes seconds, a copy install takes minutes.

For a repository-wide check, count how many files under node_modules/.pnpm have a single link:

find node_modules/.pnpm -type f -links 1 | wc -l

On a correctly configured machine using hard links, the number is close to zero; on a machine that fell back to copying, it is every file.

Store maintenance

Moving the store or changing import methods leaves old data behind. pnpm store prune removes packages that no project references anymore, which reclaims space after large upgrades or after deleting projects. Run it occasionally rather than on every install, because pruning packages that a project on a switched-off branch still needs means that project downloads them again next time. If you created a new store on a different disk, delete the old one once every project has been reinstalled against the new location — until then, some projects may still link into it.

A note on editing files in node_modules

Hard links share the same file contents with the store. If you edit a file inside node_modules for debugging, you are editing the store copy, which affects every project linked to it on the machine. pnpm verifies store integrity and repairs modified files on later installs, but the edit can confuse other projects in the meantime. Use pnpm patch for intentional changes, which creates a patched copy instead of editing the shared file. Cloning (clone or clone-or-copy) avoids this hazard entirely on filesystems that support it.

Worked example: a slow install on a new laptop

A developer moves their projects to a second SSD mounted at /mnt/work and finds that pnpm install now takes nearly two minutes instead of seconds, with node_modules folders of several gigabytes each. pnpm store path shows the store under the home directory on the system disk. Setting store-dir=/mnt/work/.pnpm-store in ~/.npmrc and reinstalling brings installs back to a few seconds and cuts total disk usage across their twenty projects by more than 80%, because every project now links into one store on the same disk.

Prevention and guardrails

  • Keep the store on the same filesystem as your projects, set once per machine in the user .npmrc.
  • Use package-import-method=hardlink on developer machines if you want fallbacks to fail loudly.
  • On WSL, work inside the Linux filesystem.
  • In CI and Docker, cache the store, not node_modules, on the same mount as the workspace.

Frequently Asked Questions

Is copying ever the right choice? Yes, when the destination must be independent of the store — for example, a node_modules folder that will be archived and deployed. pnpm deploy and container builds produce independent files for exactly that reason.

Does pnpm support multiple stores? Yes. Each filesystem can have its own store; pnpm uses the configured store-dir, and by default creates one per filesystem when it can. Disk savings only apply within a store.

Why does the warning appear only sometimes? pnpm warns when it falls back during an install that needed to import packages. A warm install where nothing changed imports nothing and prints no warning, even though the existing files are copies.

Related

Workspace Symlinks vs Hard Links