The short version
- Every project keeps its own full copy of its dependencies in
node_modules. With many projects, that duplication dominates. node_modulesholds nothing you wrote. It is always restored byinstall, so it is safe to delete in projects you are not actively building.- The global package caches (
~/.npm, the pnpm store) are separate, shared, and self-healing. - The safe strategy is to target idle projects, since an active one simply reinstalls on the next build.
Why it gets so big
The JavaScript ecosystem favours small, single-purpose packages, and those packages depend on other packages, which depend on still more. A modest web app can pull in hundreds or thousands of them. That alone would be manageable if there were one copy. The real multiplier is that, with the traditional npm and Yarn Classic layout, every project gets its own complete copy.
The pnpm team put the cost in one sentence: "if you have 100 projects using a dependency, you will have 100 copies of that dependency saved on disk." A single modern app's node_modules commonly runs 200 to 500 MB; ten or twenty projects routinely add up to somewhere between 5 and 30 GB. Developers regularly report freeing 15 GB or more just by sweeping up stale ones.
node_modules is the other.The key distinction: project folders vs shared caches
There are two separate things people lump together as "npm junk," and they behave very differently.
| What | Where | Reclaim |
|---|---|---|
node_modules | inside each project | Biggest win. Restored by install. |
| npm cache | ~/.npm | Shared download cache. Self-healing. |
| pnpm store | ~/Library/pnpm/store | Shared, hard-linked. Prune unreferenced. |
| Yarn cache | varies by version | Classic is global; Berry is per-project. |
The per-project node_modules folders are where the real space is, and they are the safest to delete, because they contain nothing you authored and are rebuilt by a single command.
Clearing node_modules safely
For one project, it is trivial: delete the folder, and reinstall when you next work on it.
# one project $ rm -rf node_modules $ npm install # or pnpm install, or yarn, when you return to it
To see what is out there across your whole drive, find every node_modules and measure it. The -prune keeps find from descending into them, which is both faster and gives one total per project:
# list every node_modules with its size, largest first $ find ~ -name node_modules -type d -prune -exec du -sh {} + 2>/dev/null | sort -rh | head -30
Deleting node_modules from the project you are building right now just means you reinstall it in a minute. The space worth reclaiming is in projects you have not touched for months. The signal to look for is the folder's age, or better, when you last committed to that repo, not just its size.
The shared caches
After the project folders, the global caches are a smaller, second helping. The npm cache is designed to be disposable. npm's own docs say it is "self-healing and resistant to data corruption," and that clearing it "should never be necessary except to reclaim disk space," which is exactly our reason. That is why the command needs a --force.
# npm: confirm the path, then clear (safe, re-downloads on demand) $ npm config get cache $ npm cache clean --force # pnpm: remove packages no project references anymore $ pnpm store path $ pnpm store prune # yarn: find the real cache dir for your version, then clean $ yarn cache dir $ yarn cache clean
Yarn Classic (v1) keeps a single global cache. Yarn Berry (v2 and later) defaults to a per-project .yarn/cache, which multiplies across every checkout much like node_modules does. Always run yarn cache dir rather than guessing the path, because it differs by version.
The structural fix: pnpm
If the duplication really bothers you, the durable answer is to change how packages are stored. pnpm keeps one content-addressable copy of each package version in a global store and hard-links it into each project's node_modules. Switch several projects to pnpm and they share one physical copy of every shared dependency, which can cut total usage dramatically. One caveat: the store and your projects must be on the same filesystem, because hard links cannot cross one.
Because pnpm (and tools like uv) use hard links and clones, naive size tools can badly overcount, reporting the full size of every linked copy as if it were separate. A disk tool that understands clones and links will tell you the true shared cost. That is one reason "how big is node_modules really" is a harder question than it looks.
A sensible routine
- Run the
findcommand to list everynode_modulesby size. - Delete the folders in projects you have not worked on for a few months.
- Run
npm cache clean --forceandpnpm store prunefor a bit more. - Reinstall on demand the next time you open an old project.
- Consider moving long-lived projects to pnpm to stop the duplication at the source.
Reclaim idle projects, safely
StorageSage finds every node_modules, build, target, .venv and friends, matches each to its project, and reads idle time from your git history so only projects you have not touched for months are preselected. The shared caches are labelled Rebuilds, and everything goes to the Trash first.
Sources
- pnpm — motivation and store documentation (per-project duplication; shared hard-linked store;
store prune). - npm — cache documentation (self-healing cache;
npm cache clean --force). - Yarn — cache documentation for Classic and Berry (
yarn cache dir,yarn cache clean). - Developer write-ups on reclaiming space from stale
node_modules(sizes are commonly reported, not official averages).