Why pnpm Workspaces Stopped My Monorepo From Eating the Disk
pnpm workspaces store every dependency once in a content-addressable store and link it into each package, ending the monorepo habit of keeping ten copies of lodash. Here's how it works, a concrete setup, and the flat-tree trade-off.
13 Mar 2026, 04:55 UTC

If you run a monorepo with npm or classic Yarn, every package in it gets its own physical copy of every dependency. Ten packages depending on lodash means ten copies of lodash on disk — plus ten copies of everything lodash's metadata drags along. Multiply that across a real dependency tree and a modest monorepo can burn tens of gigabytes on duplicated files, while install times crawl.
pnpm workspaces fix this with a simple idea: store each package exactly once, then make it look like it exists everywhere. This post walks through how that works, a concrete two-package setup, the one trade-off that bites people, and how to verify the savings yourself.
One store, many symlinks
pnpm keeps a single content-addressable store on your machine (usually ~/.pnpm-store). "Content-addressable" means files are named by the hash of their contents, so lodash@4.17.21 exists exactly once, no matter how many projects use it.
When you run pnpm install in a workspace, pnpm builds each package's node_modules out of hard links into that store, plus a node_modules/.pnpm directory of symlinks that reconstructs the dependency graph. The result is a non-flat layout: a package can only require the dependencies it actually declares, which also catches phantom-dependency bugs that flat layouts hide.
A worked example: ui + api sharing lodash
Assume a repo with two packages, packages/ui and packages/api, both declaring lodash@^4.17.21. At the repo root, create pnpm-workspace.yaml:
packages:
- 'packages/*'Then, from the repo root (no special permissions needed; pnpm must be installed, e.g. via corepack enable && corepack prepare pnpm@latest --activate on Node 16.13+):
pnpm installAfter install, the store holds one physical copy of lodash. Both packages/ui/node_modules/lodash and packages/api/node_modules/lodash resolve through .pnpm symlinks to that single copy. You can confirm the deduplication from the root:
pnpm ls lodash --recursive
du -sh ~/.pnpm-store
du -sh node_modules packages/*/node_modulesThe first command should show lodash resolved to one version across both workspaces. The du numbers are the real payoff: compare them against a scratch clone installed with plain npm, and the difference is typically dramatic on anything beyond a toy repo. (Exact numbers depend on your dependency tree, so measure your own rather than trusting anyone's benchmark — including this one.)
Cross-package dependencies work the same way. If api depends on ui, declare it with "ui": "workspace:*" and pnpm links the local package instead of hitting the registry.
The trade-off: tools that expect a flat tree
The symlinked, non-flat layout is the feature — and the friction. Some older build tools, native-module build scripts, and plugins assume they can reach any package by walking up node_modules, or resolve sibling packages via relative paths. Those can break under pnpm because undeclared packages simply aren't visible.
You have three options, in order of preference:
- Fix the dependency declaration. If your code requires something, add it to
package.json. Most "pnpm broke my build" reports are actually latent bugs. - Configure the tool to follow symlinks. Many bundlers and test runners have a
preserveSymlinks-style option; the default usually works with pnpm, but check when something misbehaves. - Hoist as an escape hatch. Setting
shamefully-hoist=truein.npmrcflattens everything into the rootnode_modules, mimicking npm. It unblocks legacy tooling but reintroduces phantom-dependency risk and weakens isolation, so treat it as a migration aid, not a destination.
One more operational note: all workspaces share a single pnpm-lock.yaml at the root. Bumping a dependency in one package updates the shared lockfile, so run pnpm install from the root afterward and commit the lockfile change with the bump, or CI and local checkouts will drift apart.
Try it on one repo this week
Pick your most dependency-heavy monorepo, add pnpm-workspace.yaml, delete the old node_modules folders and lockfiles, and run pnpm install. Then verify with the three checks above: ls ~/.pnpm-store shows hash-named folders, pnpm ls -r shows shared packages resolved once, and du -sh shows the disk savings. If a legacy tool complains, reach for a config fix before shamefully-hoist. The disk space — and the stricter dependency hygiene — are worth the one-afternoon migration.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.