Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Deployment

npm Clean Install: When Deleting node_modules Is Right, When `npm ci` Is Right, and When Both Are Slowing You Down

Sean

Platform Writer

Jun 17, 2026
7 min read

There are four ways to do a clean install in npm, and they have different semantics. npm install resolves dependencies against the lockfile, npm ci enforces the lockfile and refuses to deviate, rm -rf node_modules && npm install purges the cache and the install state, and npm install --force overrides peer-dependency and version conflicts. The right one depends on what you are trying to fix. Most of the “delete node_modules and reinstall” advice that lives in Stack Overflow answers is right about half the time and slow the other half. The trick is matching the command to the failure mode.

This post is the decision tree. The first half is what each command actually does, including the failure mode it fixes. The second half is the trap of treating “clean install” as a single concept, and the right answer for CI, for local development, and for the next time the build mysteriously fails.

The interesting thing about npm clean install is that the name is misleading. npm install is not clean — it resolves, it merges, it can introduce drift. npm ci is the clean one. The Stack Overflow answer that says “delete node_modules and reinstall” is doing what npm ci does, except slower and with more risk of drift. The right answer for most “clean install” problems is npm ci, not the manual delete.

npm Clean Install: When Deleting node_modules Is Right, When `npm ci` Is Right, and When Both Are Slowing You Down

Table of contents

The direct answer

For most “clean install” problems, npm ci is the right answer:

# Fast, deterministic, refuses to deviate from the lockfile
npm ci

For cases where the local install is in a weird state (corrupted cache, weird node_modules from a half-completed install), the manual purge:

# Slow, but starts from scratch
rm -rf node_modules
npm ci

For the rare case where peer-dependency or version conflicts are blocking the install, the override:

# Last resort — overrides npm's safety checks
npm install --force

The rest of the post is the decision tree for picking among these.

What “clean install” actually means

The phrase “clean install” is overloaded. In the strict sense, a clean install is one that:

  • Installs only the dependencies in the lockfile, at the exact versions in the lockfile.
  • Does not write to package.json.
  • Does not update the lockfile.
  • Refuses to proceed if the lockfile and package.json are out of sync.

In the loose sense, “clean install” is what Stack Overflow tells you to do when something is broken: delete node_modules and reinstall. The loose sense is closer to “wipe the local state and try again” than to a strict install.

The two senses are not the same. npm install is the loose sense — it reinstalls, but it can also write to the lockfile, merge in new dependencies, and change behavior between runs. npm ci is the strict sense — it is fast, deterministic, and refuses to deviate.

When the problem is “the install is in a weird state,” the loose sense is the answer. When the problem is “I need this exact dependency set, exactly as the lockfile says,” the strict sense is the answer. The mistake is to reach for the loose sense when the strict sense is what the situation calls for.

Command 1: npm install (the default)

npm install is the default. It does five things:

  • Reads package.json and package-lock.json.
  • Resolves the dependency graph (using the lockfile as a hint, but capable of deviating from it).
  • Downloads the dependencies to node_modules.
  • Updates the lockfile if the resolved set differs from the lockfile.
  • Runs prepare, postinstall, and other lifecycle scripts.

The first three are what most people think npm install does. The last two are the part that surprises people. npm install can change the lockfile, and npm install runs scripts from the dependencies it installs. Both are part of the npm model and both are a security and reproducibility concern.

npm install is the right answer for:

  • Local development, when the developer wants to add a new dependency.
  • Local development, when the developer wants to update an existing dependency.
  • Any time the team is intentionally changing the dependency set.

npm install is the wrong answer for:

  • CI builds. CI should not change the lockfile, and npm install can.
  • Production deploys. Production should install exactly what the lockfile says.
  • Reproducing a bug. The bug should be reproducible from the lockfile, not from a fresh resolve.

Command 2: npm ci (the actually-clean one)

npm ci is the strict sense of “clean install.” It does four things:

  • Reads package.json and package-lock.json.
  • Verifies that package-lock.json is consistent with package.json (refuses if not).
  • Deletes node_modules (if it exists) before installing.
  • Installs exactly the versions in the lockfile, no resolution, no deviation.

npm ci does not write to the lockfile. npm ci does not run lifecycle scripts (other than the prepare hooks, which is the only one that runs). npm ci is fast — typically 2-3x faster than npm install because it skips the resolution step entirely.

npm ci is the right answer for:

  • CI builds. The build installs exactly what the lockfile says, no deviation, no lockfile mutation.
  • Production deploys. The runtime installs exactly what the build installed.
  • Reproducing a bug. The team can check out the lockfile and run npm ci to get the same node_modules the original developer had.

npm ci is the wrong answer for:

  • Local development, when the developer wants to add a new dependency. npm ci will not update the lockfile; the developer needs npm install <package> for that.
  • Any time the team is intentionally changing the dependency set.

The trap: npm ci will refuse to run if the lockfile and package.json are out of sync. The error is “Your lockfile needs to be updated.” The fix is npm install (which updates the lockfile) followed by npm ci (which uses the new lockfile). The fix is two commands, not one.

Command 3: rm -rf node_modules (the manual purge)

The Stack Overflow classic. The developer suspects a corrupted node_modules and nukes it before reinstalling.

What it actually does:

  • Deletes the node_modules directory.
  • The next npm install or npm ci reinstalls from scratch.

When this is the right answer:

  • node_modules is in a corrupted state (half-completed install, file system errors, dependency conflicts from a previous install that left orphan files).
  • The team has tried npm ci and the build still fails.
  • The team is debugging a “works on my machine” problem and wants to start from a known state.

When this is the wrong answer:

  • For routine CI builds. npm ci already deletes node_modules before installing. The manual delete is redundant and slower.
  • For local development, when the problem is in the lockfile or in the source code. The node_modules is not the issue.
  • As a habit. The manual delete is slow, and the slow habit is the cost the team pays for a problem that npm ci solves.

The best practice: in CI, do npm ci. In local development, do rm -rf node_modules && npm ci only when npm ci alone does not fix the issue. The escalation order is npm ci → manual delete + npm cinpm ci --forcenpm install --force.

Command 4: npm install —force (the override)

npm install --force is the last resort. It does what npm install does, but it overrides the safety checks that would otherwise refuse the install.

The safety checks it overrides:

  • Peer-dependency conflicts (a package wants a different version of a peer than is installed).
  • Version mismatches between package.json and the lockfile.
  • Some engine mismatches (a package wants a different Node version than the running Node).

--force should be used carefully. The peer-dependency conflict it overrides is sometimes a real bug in the dependency graph, and the override hides the bug. The team that uses --force regularly is the team that ships a bug because the install succeeded but the runtime behavior is wrong.

When this is the right answer:

  • A peer-dependency conflict that the team has investigated and confirmed is a false positive.
  • A version mismatch that the team has investigated and confirmed is benign.
  • A desperate last resort before giving up on npm install and switching to a different package manager.

When this is the wrong answer:

  • As a habit. The override is for cases the team has investigated, not for cases the team is impatient with.
  • For builds that ship to production. The override is a debugging tool, not a deploy tool.

The safer alternative: npm install --legacy-peer-deps. This is the same as --force but only for peer-dependency conflicts, leaving the other safety checks in place. The team that needs to override peer-deps should use --legacy-peer-deps, not --force.

The decision tree

The decision tree for picking the right command:

  1. Are you changing the dependency set? (Adding a package, removing a package, updating a version.) Use npm install <package> or npm install <package>@version.
  2. Are you in CI? Use npm ci. Always.
  3. Is the local install in a weird state? Try npm ci first. If that fails, rm -rf node_modules && npm ci.
  4. Is the lockfile out of sync with package.json? Run npm install to update the lockfile, then npm ci to use it.
  5. Are peer-dependency conflicts blocking the install? Investigate first. If the conflict is a false positive, use npm install --legacy-peer-deps. Avoid --force unless you have a specific reason.
  6. Is everything else failing? Use npm install --force as a last resort. The override is for debugging, not for production.

The default for most “clean install” questions is npm ci. The escalation path is small.

The CI vs local distinction

The CI vs local distinction is the part of the decision tree most teams get wrong.

CI behavior should be: npm ci. The build installs exactly what the lockfile says. The lockfile does not change. The build is reproducible. The team can deploy the same artifact to staging and to production because both built from the same lockfile.

Local behavior should be: npm install <package> when adding or updating. npm ci when the install is in a weird state. The lockfile can change locally, and the team commits the updated lockfile with the dependency change.

The trap: the team uses npm install in CI, the lockfile changes in the build, and the next developer’s local install is different from the CI build. The “works on my machine” bug is back. The fix is to use npm ci in CI, not to use npm install everywhere.

A platform that runs the CI build with npm ci and caches the result by the lockfile hash is the platform where CI is fast and reproducible. The RunxBuild platform handles this by default. The team’s .nvmrc pins the Node version, the build runs npm ci, the cache is keyed to the lockfile, and the deploy picks up the exact same artifact that the CI built.

The cache trap

The cache trap is the case where the install is fast because npm cached a half-correct result, and the team does not notice until production.

The pattern: npm install resolves a dependency, downloads it, and caches the result. The next npm install finds the cached version and uses it. The cache is keyed to the package version, the lockfile, and the Node version. The cache works for 99% of installs. The 1% is the case where the cache is wrong — a corrupted file, a half-downloaded package, a version mismatch the cache did not detect.

The fix is npm ci, which deletes node_modules and installs from the lockfile without using the resolution cache. The fix is also the manual rm -rf node_modules && npm ci, which does the same thing with more steps.

The deeper fix: the CI cache is keyed to the lockfile hash. The build runs npm ci. The build is reproducible. The team does not need to clear the cache because the cache is rebuilt every time the lockfile changes. The trap is gone because the cache is correct by construction.

The opinion this post is built on

The reason “delete node_modules and reinstall” is the canonical Stack Overflow answer is that it works in more cases than npm install alone, and the cases where it does not work are rare. The problem is that it is slow, it does not address the root cause, and it teaches the team to reach for the slow hammer instead of the right tool.

The right tool is npm ci. It is fast, it is deterministic, it enforces the lockfile, and it is the command that CI should be running anyway. The team that uses npm ci as the default rarely needs the manual delete. The team that uses npm install as the default often does.

The platform is the multiplier. A platform that runs npm ci in CI, keys the cache to the lockfile, and rebuilds on dependency changes is a platform where the “delete node_modules and reinstall” advice is irrelevant. The build is fast, the build is reproducible, and the local install is what the CI built. The trap is gone by construction.

The deeper discipline: the lockfile is the contract. The team’s package.json is the intent. npm ci enforces the contract. npm install negotiates. CI should enforce; local development should negotiate. The two commands have different jobs, and the team that uses the right one in the right place is the team that does not have “works on my machine” bugs.

How this fits the rest of the stack

A reproducible install is also a build cost — the install time, the cache size, the bandwidth, and the storage for the lockfile and the cache each show up as a line item. The team’s mental model for the build cost is the sum of those numbers, and the team should know the total before adding the next dependency or the next runner. The RunxBuild hosting calculator is the right place to model that — pick the build frequency, the install size, the cache size, and the bandwidth, and the calculator shows what the build pipeline costs at the team’s actual usage.

Useful related references:

FAQ

What is the difference between npm install and npm ci?

npm install resolves dependencies against the lockfile (but can deviate from it), can write to the lockfile, and runs lifecycle scripts. npm ci enforces the lockfile, refuses to deviate, deletes node_modules before installing, and skips most lifecycle scripts. npm ci is fast and deterministic. npm install is flexible and can mutate state.

When should I use npm ci instead of npm install?

Use npm ci in CI, in production deploys, and any time the team wants to install exactly what the lockfile says. Use npm install when adding or updating dependencies locally, and when the team is intentionally changing the dependency set.

Why is rm -rf node_modules && npm install slow?

Because it runs npm install, which resolves dependencies (slow) and downloads them (also slow). npm ci skips the resolution step (fast) and uses the lockfile directly. For the same goal — a clean install from the lockfile — npm ci is 2-3x faster than the manual delete plus npm install.

Should I commit package-lock.json?

Yes, always. The lockfile is the contract for the dependency set. Without it, every npm install resolves a possibly-different set of versions, and the team’s “works on my machine” bug is back. Commit the lockfile, use npm ci in CI, and the build is reproducible.

What is npm install --legacy-peer-deps?

It is npm install with the peer-dependency check disabled. npm 7+ enforces peer-dependency conflicts by default; --legacy-peer-deps reverts to the npm 6 behavior of installing anyway. Use it when the team has investigated a peer-dependency conflict and confirmed it is a false positive. Do not use it as a habit.

What is npm install --force?

It is npm install with most safety checks disabled. It overrides peer-dependency conflicts, version mismatches, and engine mismatches. Use it as a last resort, after the team has investigated the failure and confirmed that --force is the right escape hatch. Avoid using it in production deploys.

#npm clean install#npm ci vs npm install#remove node_modules#npm ci clean#npm install --force