skip to content

A CI pipeline switches from `npm install` to `npm ci` (or from `yarn install` to `yarn install --frozen-lockfile`) specifically for build reproducibility. What does this strict install mode actually do differently under the hood, and what class of bug does it catch that the regular install command lets through silently?

level: seniorimportance: should knowfreq 55%

answer

  1. wipes node_modules, no resolution, just replays lockfile
  2. hard-fails on manifest/lockfile drift instead of reconciling
  3. faster because it skips the resolve step
  4. requires a lockfile to already exist

basics

~20 s

Strict install modes wipe out existing node_modules and reinstall purely from the lockfile, and they refuse to run at all if the lockfile doesn't exactly match the manifest -- catching cases where someone forgot to update the lockfile after changing dependencies, instead of silently patching the mismatch like a normal install would.

solid answer

~40 s

`npm ci` deletes any existing node_modules directory and installs strictly from package-lock.json without performing any dependency resolution -- it treats the lockfile as the complete, authoritative answer and simply fetches and lays out exactly what's recorded. Critically, it first validates that the lockfile's dependency declarations are consistent with package.json, and if they're out of sync -- e.g. someone added a dependency to package.json but forgot to run install to regenerate the lockfile -- it fails immediately rather than silently reconciling the mismatch the way a plain `npm install` would. This makes it strictly faster (no resolution step) and strictly safer for CI: any drift between manifest and lockfile becomes a hard build failure that surfaces in review, instead of an untested dependency graph slipping into what gets built and deployed.

go deeper

for a junior

Knows CI should use a different, stricter install command than what's typical for local development, without necessarily knowing the exact mechanics.

for a middle

Can describe that the strict command fails on manifest/lockfile mismatch and skips resolution, and knows to fix drift by running a regular install locally.

for a senior

Explains the full mechanism (clean node_modules wipe, no resolution, hard validation) and can reason about why this specifically belongs in CI/build stages rather than local dev.

for a principal

Enforces this as an org-wide CI policy (e.g., via a shared pipeline template) and can articulate the risk this closes -- untested dependency drift reaching production -- alongside the operational cost of onboarding teams onto stricter installs.

## A regular install reconciles A regular `npm install` is fundamentally an **incremental, reconciling operation**: it looks at what's already in `node_modules` and the existing lockfile, compares that against `package.json`, and does the minimum work needed to make everything consistent again -- resolving any new or changed ranges, potentially updating the lockfile in the process, and leaving untouched packages alone. That reconciliation is exactly what you want on a developer's laptop day to day (fast, incremental, forgiving), but it's precisely the wrong behavior for a build you intend to promote to production, because 'reconciling' means the installer is allowed to make resolution decisions on your behalf, silently, at build time. ## The three things a strict install does differently `npm ci` (and yarn's frozen-lockfile mode, and pnpm's equivalent) is built for the opposite goal: total determinism with a hard failure on any ambiguity. Concretely, it does three things differently. 1. **First, it deletes any existing `node_modules` directory before installing,** so there's no possibility of stale, hand-modified, or partially-installed leftover files influencing the result -- every build starts from zero. 2. **Second, it skips dependency resolution entirely:** rather than computing which version satisfies each range, it just reads the exact versions and integrity hashes already recorded in the lockfile and installs precisely those, which is also why it's noticeably faster than a full install on a cold cache. 3. **Third** -- and this is the part that actually changes developer behavior -- before doing any of that, it validates that the lockfile is fully consistent with `package.json`: same set of declared dependencies, same version ranges. If there's any discrepancy, `npm ci` refuses to proceed and exits with an error, rather than treating the mismatch as something to fix on the fly. ## Why the strict mode exists **Why this exists:** the whole point of a lockfile is to guarantee 'what gets built is exactly what was reviewed,' and a reconciling install command quietly undermines that guarantee the moment the lockfile and manifest disagree, because it will paper over the disagreement by re-resolving -- possibly pulling in a newer transitive version that was never reviewed in the PR that changed `package.json`. In a CI context specifically, that means a build could pass using a dependency graph nobody actually looked at, and that drift is invisible unless someone happens to notice an unexpected diff in the lockfile after the build runs. By making that exact scenario a hard failure instead, strict install modes force the discrepancy to be caught locally, before merge -- the fix is simply to run a regular install locally to regenerate the lockfile and commit that. ## The trade-off The trade-off is minor and almost entirely upside for CI/production use: the only real cost is that `npm ci` requires a lockfile to exist at all (it errors out if there isn't one) and is deliberately less forgiving, so if a developer legitimately wants to experiment with dependency changes without regenerating the lockfile yet, `npm ci` isn't the right command for that -- you'd use a regular install locally and switch to `npm ci` only in CI/build/deploy stages. There's no meaningful reproducibility trade-off; it's strictly a stricter, faster subset of what a full install does when the lockfile and manifest already agree. ## The failure this catches in practice The failure mode this catches in practice, very concretely: an engineer adds a new package by directly hand-editing `package.json` (or resolves a merge conflict in `package.json` without re-running install afterward) and pushes without realizing the lockfile wasn't regenerated. - **With a plain `npm install` in CI,** the build 'just works' -- the CI machine quietly resolves the new dependency's version and the build succeeds, but the exact version that got installed was never actually reviewed by a teammate and isn't reproducible from the merged commit alone; a rebuild six months later from that same commit could resolve to something different again. - **With `npm ci`,** that same PR fails CI outright with a clear, actionable error pointing at the mismatch, which is the correct outcome: it forces the lockfile regeneration to happen as a reviewable diff in the PR itself. This is precisely why standard CI reference guidance (npm's own docs, common GitHub Actions Node.js setup guidance, and standard Docker multi-stage build patterns for Node apps) recommends the strict install for automated environments and reserves the regular install for local development.

  • If `npm ci` fails due to lockfile drift, what's the correct fix, and what's the anti-pattern fix engineers sometimes reach for instead?
    The correct fix is to run a regular install locally to let the lockfile reconcile against the manifest, review the resulting lockfile diff, and commit it. An anti-pattern some teams reach for is deleting the lockfile and regenerating it wholesale or switching CI back to plain install, both of which either discard useful history/pinning or quietly reintroduce the exact non-determinism the strict command exists to prevent.
  • Why does deleting node_modules before installing matter for reproducibility, beyond just using the lockfile?
    A long-lived node_modules directory on a CI runner or shared cache can accumulate stale files from previous dependency versions, manually patched packages, or partial installs from an interrupted prior run; starting from a clean slate every time removes any chance those leftovers silently influence the current build's behavior.
  • Does `npm ci` protect against a scenario where the lockfile itself was tampered with or hand-edited incorrectly, independent of package.json?
    Not directly for consistency with package.json in that specific case since the lockfile is the authoritative source it trusts, but the integrity hashes inside the lockfile still get checked against whatever's actually downloaded, so a hand-edited lockfile pointing at a wrong version would still install that now-locked wrong version successfully, while a hand-edited hash that doesn't match the real package bytes would still fail the integrity check.

A regular install is like a chef improvising and adjusting a dish if an ingredient on the shopping list doesn't quite match what's in the pantry; a strict CI install is like a line cook following a plated photo exactly -- if the ingredients on hand don't match the photo's list precisely, they stop and send it back instead of quietly substituting.

saying these in an interview costs you the question

  • Thinks npm ci and npm install behave identically as long as a lockfile is present
  • Doesn't know npm ci deletes node_modules before installing
  • Believes npm ci will regenerate/fix the lockfile if it's out of sync
  • Can't explain why npm ci requires a lockfile to already exist
  • Assumes npm ci is only about speed, not about correctness/drift detection

context