How pnpm’s Content‑Addressable Store Cuts Disk Footprint in Monorepos
Duplicate dependencies in large monorepos can balloon disk usage. pnpm’s content‑addressable store and hard‑link strategy keep a single copy of each package, dramatically reducing disk space and speeding installs. Learn how it works, see a concrete example, and understand the trade‑offs.
11 Dec 2025, 02:29 UTC

The Duplicate Dependency Problem
In a typical JavaScript monorepo, dozens of packages often depend on the same libraries. With npm or Yarn, each package gets its own copy of that library inside its node_modules. If 20 services all depend on react@18.2.0, npm will create 20 separate copies, consuming ~200 MB of disk space and slowing future installs.
pnpm’s Store‑First Architecture
pnpm stores every unique package tarball once in a global content‑addressable store located at ~/.pnpm-store. The key is a hash of the package’s package.json and its tarball, so identical versions share the same file. When a project needs a dependency, pnpm hard‑links the required files from the store into the project’s node_modules.
A hard link is a file system entry that points to the same inode as the original file. Multiple directory entries can reference the same inode, so the file data is stored only once on disk. On Linux/macOS this is transparent and fast; on Windows symlinks are used as a fallback, which can be slower and requires elevated permissions.
Concrete Example: Two Services Sharing React
Suppose we have a monorepo with two services, service-a and service-b, both listing react@18.2.0 in their package.json.
// service-a/package.json
{
"name": "service-a",
"dependencies": {
"react": "18.2.0"
}
}
// service-b/package.json
{
"name": "service-b",
"dependencies": {
"react": "18.2.0"
}
}
Running pnpm install at the root will:
- Download
react@18.2.0once into~/.pnpm-store/6/18.2.0. - Create hard links in
service-a/node_modules/reactandservice-b/node_modules/reactpointing to the same inode.
Verify the hard link:
cd service-a/node_modules/react
ls -li | head -n 1
Run the same in service-b and compare the inode numbers. They should match, confirming both directories share the same underlying file.
Check the global store status:
pnpm store status
The output will list react@18.2.0 once, with a size of roughly 20 MB. After the install, service-a/node_modules and service-b/node_modules will each be a few megabytes, far less than the ~200 MB that npm would create.
Trade‑offs & Limitations
- Windows Permissions: Hard links require elevated privileges. Without them, pnpm falls back to copying files, negating disk savings. Workarounds include running
pnpm installas administrator or enabling Developer Mode to allow symlink creation. - Symlink Overhead: On Windows, symlinks are slower than hard links and can cause issues with tools that expect hard links.
- Compatibility: Some legacy build tools may not handle symlinked
node_modulescorrectly. In such cases, the--shamefully-hoistflag can be used to flatten the dependency tree, but this reintroduces duplication.
Actionable Next Steps
- Switch to pnpm: If you’re on npm or Yarn, run
pnpm add --global pnpmto install pnpm, then replacenpm installwithpnpm installin your CI scripts. - Verify Store Usage: After the first install, run
pnpm store statusand compare the size to thenode_modulesfolders. Usedu -sh node_moduleson Linux/macOS to confirm savings. - Test on Windows: If you’re on Windows, enable Developer Mode (Settings → Update & Security → For developers) or run your terminal as administrator before installing. Verify that
ls -lishows identical inode numbers for shared dependencies. - Monitor Disk Space: Add a post‑install hook that logs
du -sh ~/.pnpm-storeto your CI pipeline, ensuring the store grows only as new unique packages are added.
By leveraging pnpm’s content‑addressable store and hard‑link strategy, teams can keep monorepos lean, reduce install times, and avoid the “dependency bloat” that plagues many JavaScript projects.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.