Monorepo Disk Bloat and How pnpm's Content-Addressable Store Fixes It
pnpm stores packages once in a global content-addressable store and hardlinks them into workspaces, eliminating monorepo disk duplication—if your tools handle symlinks.
28 Nov 2025, 08:49 UTC

If you manage a monorepo with more than a handful of packages, you have probably watched node_modules swallow gigabytes of disk space for no obvious reason. The same version of TypeScript, React, or a utility library gets copied into every workspace package, and your CI cache grows until it is larger than the repository itself. The problem is not that your dependencies are heavy; it is that traditional package managers duplicate files by default.
pnpm approaches this differently. Instead of copying packages into every project, it stores each package version once in a global content-addressable store and links those files into your workspace using hardlinks. The result often reduces disk usage by 70–90% compared to npm or Yarn Classic. But the mechanism—hardlinks plus a symlink-heavy node_modules layout—has implications for tooling, Windows support, and how strictly you must declare dependencies.
Why npm and Yarn Duplicate Everything
When you run npm install in a project, npm fetches a tarball, extracts it, and writes the files into node_modules. If you have a monorepo with ten packages that all depend on the same version of Lodash, npm may write those Lodash files ten times. Yarn Classic improves on this with hoisting, pulling shared dependencies to the root node_modules, but version conflicts still force duplication deeper in the tree.
| Approach | Storage model | Typical disk impact |
|---|---|---|
| npm | Copies packages per project | High duplication |
| Yarn Classic | Hoists shared deps, copies conflicts | Moderate duplication |
| pnpm | Global store + hardlinks | Minimal duplication |
In a CI environment, this duplication is expensive. Cache archives balloon, upload and download times increase, and runners run out of disk. Locally, developers with multiple branches or projects see the same waste repeated.
How pnpm's Store and Hardlinks Work
pnpm keeps a single global store on your machine. Every package version is stored as files identified by a SHA-512 hash of their content—this is the content-addressable part. If two projects depend on the exact same package version, pnpm stores those files once and creates hardlinks from the store into each project's node_modules.
A hardlink is a filesystem reference that points to the same underlying inode as another file. It is not a copy; it is another name for the same bytes on disk. Because the store and your local node_modules usually live on the same volume, hardlinks work without extra space.
Inside node_modules, pnpm does not simply flatten everything. It creates a .pnpm directory containing the actual package contents, then uses symlinks to wire those packages into the dependency tree your code expects. This preserves the nested node_modules layout that Node.js resolution algorithms rely on, while avoiding duplication.
You can locate the store by running this in your terminal:
pnpm store path
This prints the absolute path to the global store. On Linux and macOS it typically lives under a .pnpm-store directory in your home folder; on Windows it uses a different location because of filesystem constraints. Recent pnpm versions have kept this behavior stable, but you should confirm the path on your own machine rather than assuming a fixed location.
A Practical Monorepo Example
Suppose you have a monorepo with two packages, api and web, both depending on typescript and ramda. You want to migrate from npm workspaces to pnpm to cut disk usage.
First, create a pnpm-workspace.yaml at the repository root:
packages:
- 'packages/*'
Remove the existing node_modules directories and lockfiles, then install:
pnpm install
pnpm reads the workspace file, resolves dependencies, populates the global store, and links packages into each workspace. To verify the store is being used, check its path and inspect the disk footprint:
pnpm store path
du -sh node_modules
If you previously measured node_modules with npm, you should see a significant drop. Note that the global store itself consumes space, but it is shared across all projects on the machine, not duplicated per workspace. To confirm the workspace protocol works, run a recursive build:
pnpm -r build
If your packages reference each other with workspace:*, pnpm links them locally rather than fetching from the registry. To revert this change, restore your previous lockfile and reinstall with your original package manager.
Trade-offs You Should Understand
The symlink-heavy layout is the main source of friction. Some legacy build tools, older versions of Webpack, or certain native modules assume a flat, copied node_modules tree and may follow symlinks incorrectly. If a tool resolves a path and then writes to it expecting a real file, it might modify the store instead of a local copy.
Windows adds another constraint. Deep symlink chains can hit filesystem limitations, and creating symlinks may require elevated privileges or developer mode enabled. If your team develops on Windows, test the toolchain before rolling pnpm out broadly.
Finally, pnpm enforces a strict dependency model. Phantom dependencies—packages your code imports but that are not declared in package.json—will fail. This is intentional and improves reproducibility, but it means existing projects may need dependency declarations added before they build cleanly.
An Actionable Next Step
Before migrating an entire monorepo, measure your current footprint and test one package. Run du -sh node_modules on a single workspace, install with pnpm, and run the same command again. Then execute your build and test suite. If legacy tooling breaks, the issue is almost always symlink resolution, not the store itself. Keeping the global store on the same volume as your projects preserves hardlink behavior; moving it to a different disk turns links into copies and erases the savings.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.