package-lock.json should be committed to git for almost every project you actually deploy. The only real exception is a project that ships as a library other people install into their own apps, and even there, the exception is narrower than most blog posts make it sound. The reason is that the lockfile is not “the dependencies the project uses” — the lockfile is “the exact versions of the dependencies every other machine needs to install to produce the same node_modules you have.” Those are two different things, and they need to be different files.
This post answers the question directly, then walks through the four scenarios that determine the right answer for your project. By the end you’ll know which one you are, and you can stop reading Stack Overflow threads where everyone is right and nobody agrees.
Table of contents
- The short version, with a number
- Why the lockfile exists in the first place
- Scenario 1: An application you deploy
- Scenario 2: Internal tooling, monorepo packages, build scripts
- Scenario 3: A library published to npm
- Scenario 4: A library only used internally in a monorepo
- A decision tree that actually works
- Common arguments for “do not commit” that don’t hold up
- A short worked example, end to end
- A short worked example, end to end, for a library
- The 30-second summary
- FAQ
The short version, with a number
Of the four real scenarios a package-lock.json can live in, three are “commit it” and one is “do not commit it.” The 75/25 split is not a vote; it is a property of the problem.
| Scenario | Commit the lockfile? | Why |
|---|---|---|
| Application (web app, API, CLI, mobile app, script) | Yes | You need every developer and every deploy to install the exact same tree. |
| Internal tooling (CI scripts, build tools, monorepo packages that don’t get published) | Yes | Same reason as an application. |
| Library that is published to npm and depended on by other projects | No | The lockfile causes problems for consumers without helping you. |
| Library that is published to npm but only used internally in a monorepo | Yes | The “internal” use case wins; commit it in the monorepo. |
The answer to “should package-lock.json be committed” is almost always yes, and the question that actually matters is “what kind of project is this?” If you can answer that question, you have the answer.
Why the lockfile exists in the first place
To understand why you should commit it, it helps to understand what it is for, and what it is for is one of the most underappreciated guarantees in modern software.
When you run npm install in a project that has only a package.json, npm reads the version ranges in package.json, contacts the npm registry, and resolves a tree of dependencies that satisfies every range. The resolution is deterministic in theory, but in practice it is not. Two developers running npm install on the same package.json five minutes apart can produce different node_modules trees if a new version of a transitive dependency was published in between, or if npm’s resolution algorithm changed, or if a registry outage caused a fallback to a cached version that was slightly out of date.
This is the bug the lockfile was created to fix. package-lock.json is npm’s exact record of the tree it produced when you last ran npm install. It includes the version of every direct dependency, the version of every transitive dependency, the resolved URL for every package, the integrity hash for every package, and a few other things. When you run npm install in a project that has a lockfile, npm reads the lockfile, downloads the exact versions specified, and uses the integrity hashes to verify that what you downloaded is what the lockfile said to download. The result is bit-for-bit reproducible installs across machines, time, and networks.
That guarantee is the entire reason the lockfile exists. The lockfile is not a redundant copy of package.json; it is a stricter, more specific statement of what the project needs. package.json says “react@^18.2.0.” package-lock.json says “[email protected], downloaded from https://registry.npmjs.org/react/-/react-18.2.0.tgz, with integrity sha512-…”. The first is a range. The second is a commitment.
Scenario 1: An application you deploy
This is the 60% case, and the answer is unambiguous: commit the lockfile.
The reason is that the lockfile is what makes your deploys reproducible. A CI pipeline that runs npm install and then npm run build is making a promise: “given the current state of the repo, the build will produce the same output every time.” That promise is only true if the lockfile is in the repo, because npm install without a lockfile will produce a different tree than the one you tested locally, and the build will use whatever new transitive versions npm resolved to at build time.
This matters more than people think. The build that works on your laptop on Monday morning is not the build that works on the CI runner on Monday afternoon if a new minor version of a transitive dependency was published in between. The lockfile is what guarantees that the CI build uses the same dependency tree you tested.
It also matters for security. The integrity hashes in the lockfile are npm’s check that the package you downloaded is the package the lockfile said to download. If a registry mirror serves a tampered copy of a popular package, or if a typosquatting attack replaces a real package with a lookalike, the integrity hash check fails and the install refuses to proceed. Without the lockfile, the integrity check is bypassed.
The reason most teams get this right by default is that npm init and git init both create the lockfile and the git repo in the same minute, and the lockfile shows up in the first git status. The reason most teams get it wrong is that some senior engineer once told them “the lockfile is for your machine, not the repo” and they’ve been ignoring it ever since. That advice was almost certainly about the library exception below, not the application case.
Scenario 2: Internal tooling, monorepo packages, build scripts
The 15% case. The answer is also yes, for the same reason as an application: the build needs to be reproducible.
A monorepo with internal packages is a deployment surface, even if the packages are not published to npm. The CI pipeline that builds the monorepo, the developer who runs npm install after pulling the latest changes, the release script that bumps internal package versions — all of them are working with the same dependency tree. The lockfile makes all of them agree.
The complication that comes up in monorepos is the lockfile per package vs. lockfile at the root. npm 7+ creates a single root package-lock.json that records the entire workspace tree, plus per-package package-lock.json files for any package that has its own dependencies. Modern practice: commit both. The root lockfile keeps the monorepo install reproducible; the per-package lockfile lets individual packages be developed and tested in isolation.
Some teams use a tool like Lerna, Nx, or Turborepo to manage the monorepo. The advice is the same: commit every lockfile the tool generates. The tool may give you a pnpm-lock.yaml instead of package-lock.json if you switch to pnpm; the principle — commit the lockfile, let the tool use it — is identical.
Scenario 3: A library published to npm
The 10% case, and the one that is the source of most of the online confusion. The answer here is no: do not commit the lockfile.
A library is installed by other projects. Those projects have their own lockfiles. They have their own dependency resolution rules. They have their own opinions about which version of your library to install. If you commit package-lock.json for a library and publish it to npm, you are doing two unhelpful things at once.
First, you are publishing a file that describes your development environment, not the consumer’s production environment. Your lockfile might say you depend on [email protected] because that’s what you have installed. The consumer’s lockfile might say they have [email protected] because that’s what they resolved. If your library then require('lodash') at runtime, the consumer gets their version, not yours, and the lockfile you published is not telling them anything useful. The lockfile is for installing a tree; consumers don’t install your tree, they install their own.
Second, the lockfile can confuse the publishing tool. npm publish will by default include files matched by the files field in package.json (or the default set if you don’t set one). The default set includes package-lock.json. If a consumer’s tool does a strict install based on your lockfile, they will get a tree that includes your development dependencies, which is not what they want. This is the source of the famous “my npm install broke because of a transitive dependency in a library” bug.
The fix is the .npmignore file (or the files field in package.json). List every file that should be published. By default you want:
index.js,dist/,lib/, or whatever contains the actual build output.README.mdandLICENSE.- The relevant slice of
package.json(the name, version, peer dependencies, etc.).
Everything else — node_modules/, the lockfile, your test files, your source files if you ship build output separately, your .env, your editor config — should be excluded.
The lockfile is still useful in this scenario, just not in the repo. It should live in the CI pipeline’s cache, where it is used to make the build reproducible without leaking into the published artifact. The library’s repo can have a package-lock.json in .gitignore; the build server has the same lockfile in its build cache.
The Dev.to article “When not to use package-lock.json” by Gajus (linked in the references) is the canonical source for this position. The argument is correct for libraries and only for libraries.
Scenario 4: A library only used internally in a monorepo
The 5% case, and the one that makes the simple “yes for applications, no for libraries” rule almost-but-not-quite-right.
If you have a monorepo where one workspace package is technically “a library” (it has a name, a version, and an exports field, and other workspaces import it), but it is never published to npm and never installed by anyone outside the monorepo, the lockfile is for the monorepo’s benefit. Commit it.
The reason is that the monorepo install still needs to be reproducible. The “library” workspace might have its own devDependencies (test frameworks, type definitions, build tools) that the monorepo needs to install consistently across developers and CI. The lockfile is what makes that work.
The mental model is “would I tell a new developer to run npm install in this directory and expect the same tree I’d get?” If yes, commit the lockfile. If the package is in the monorepo, the answer is yes.
A decision tree that actually works
If you want a one-paragraph decision procedure instead of the four-scenario breakdown, here it is:
- Will anyone install this project as a dependency in another project? (Check
npm install your-project, notnpm installin the project itself.) If no, commit the lockfile. - If yes, will the install in step 1 ever produce a tree that the project’s own CI build did not test? If no, commit the lockfile. If yes, do not commit the lockfile and add the file to
.npmignoreor thefilesfield inpackage.json.
The “will the install in step 1 ever produce a tree the project’s own CI did not test” question is the trick. For a library, the consumer’s install will use the consumer’s lockfile, not yours, and the consumer’s tree is not what you tested in CI. The lockfile is at best redundant and at worst misleading. For an application, the install is the project’s own install, and the lockfile is what makes the project’s install reproducible.
This is the entire decision. Everything else is a special case.
Common arguments for “do not commit” that don’t hold up
A short list, in the spirit of “things I have read on the internet that turn out to be wrong”:
“The lockfile is for your machine, not the team.” This was almost certainly written by someone who had a problem with a library publishing its lockfile and overgeneralized to every project. The lockfile is the team’s source of truth, not your personal cache. Commit it.
“It causes merge conflicts.” Yes. Merge conflicts on package-lock.json are real, and they are usually resolved by npm install followed by npm install --package-lock-only to regenerate the lockfile. This is a 30-second resolution, not a 30-minute one. The conflict is a feature, not a bug; it is forcing the team to acknowledge that two developers introduced different dependencies.
“It bloats the repo.” A typical package-lock.json for a real project is 200KB to 2MB. The repo will not notice. The CI cache will not notice. The git clone will not notice. The day you are blocked by a 2MB lockfile in the repo, you will have many other problems first.
“The build will be reproducible anyway if I just specify exact versions in package.json.” This is true in the trivial sense (writing [email protected] instead of react@^18.2.0 does prevent the install from picking a different version) and false in the practical sense (your project has hundreds of transitive dependencies, and you cannot pin every one of them by hand). The lockfile is the only practical way to make the tree reproducible.
“We use npm ci in CI, which uses the lockfile even if it’s not in the repo.” This is technically true and practically backwards. npm ci requires the lockfile to be present. If the lockfile is not in the repo, the CI runner does not have it either, and npm ci fails. The lockfile being “available somewhere” is not the same as the lockfile being in the repo.
A short worked example, end to end
A typical Node.js application, committed to git the right way.
.
├── src/
│ └── index.js
├── test/
│ └── index.test.js
├── .gitignore
├── .nvmrc
├── package.json
├── package-lock.json
└── README.md
package.json declares the range:
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.0",
"pg": "^8.11.0"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
package-lock.json is what the last npm install produced. Every contributor to the repo, every CI run, and every deploy uses the same lockfile to install the same tree. The file is in the repo. The file is committed. The file is the contract.
The day a new version of pg ships with a bug, the lockfile is what allows the team to say “we know the exact version that was working yesterday, and we can roll back to it” without guessing. That is the value. The 200KB of YAML is worth it.
A short worked example, end to end, for a library
A typical npm library, committed to git the right way.
.
├── lib/
│ └── index.js
├── test/
│ └── index.test.js
├── .gitignore
├── .npmignore
├── package.json
└── README.md
Notice: no package-lock.json in the repo. Notice the .npmignore (or the equivalent files field in package.json). The lockfile lives in the CI build cache, where it makes the library’s own build reproducible without ending up in the published artifact.
package.json looks the same as the application case, except for the addition of the files field that controls what gets published:
{
"name": "my-lib",
"version": "1.0.0",
"main": "lib/index.js",
"files": ["lib", "README.md", "LICENSE"],
"dependencies": {
"lodash": "^4.17.0"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
The files field is the whitelist of what gets included when the library is published to npm. lib/, README.md, LICENSE. Everything else is excluded by being absent from the list. The lockfile is not in the list; it doesn’t get published.
The CI build is reproducible because the lockfile is in the build cache, not because the lockfile is in the published artifact. The library’s users are reproducible because the library doesn’t ship its lockfile to them.
This is the only setup where not committing package-lock.json is the right call. Everywhere else, commit it.
The 30-second summary
Commit the lockfile. The cases where you should not are the cases where the project is published as a library to npm, and even there the answer is “do not publish the lockfile,” not “do not commit it in development.” When in doubt, commit the lockfile and move on to the part of the work that actually matters.
The Stack Overflow threads arguing about this for 11 years are mostly arguing about the wrong thing. The real question is “will this project be installed as a dependency in another project?” If yes, the lockfile is a publishing problem, not a committing problem. If no, commit the lockfile. That is the entire decision.
Frequently asked questions
Should package-lock.json be committed to git?
Yes, for almost every project. Commit package-lock.json for applications, internal tools, monorepo packages, and CLI tools. The only case where you do not commit it is a library that is published to npm and depended on by other projects. In that case, the lockfile is still useful in development but should not be in the published artifact; add it to .npmignore or the files field in package.json.
Why is committing package-lock.json important?
Because it makes the install reproducible. Without the lockfile, every install resolves a fresh tree of dependencies, and the new tree can differ from the one the project was tested against. With the lockfile, every install downloads the exact versions that were tested. The lockfile is also the source of the integrity hashes that protect against tampered packages.
Can I commit package-lock.json and yarn.lock at the same time?
No. The two files are produced by different package managers (npm vs. yarn) and they describe the same tree in different formats. If you have both, your contributors don’t know which one is the source of truth. Pick one. If you are starting fresh, use npm and package-lock.json. If you are in a yarn codebase, use yarn and yarn.lock. Don’t commit both.
What about pnpm-lock.yaml?
The same principle, with a different filename. If you use pnpm, commit pnpm-lock.yaml instead of package-lock.json. The lockfile is what makes the install reproducible; the tool is what reads the lockfile. pnpm and npm are not interoperable in the lockfile format, so a project that switches tools needs to regenerate the lockfile.
Does committing package-lock.json cause merge conflicts?
Yes, and you want it to. A merge conflict on package-lock.json means two developers introduced different dependencies or different versions of dependencies. The conflict is npm’s way of saying “the two trees don’t agree; pick one and run npm install.” The resolution is npm install (which updates the lockfile to match the resolved tree) and commit. The conflict is a 30-second resolution that catches a real divergence.
What happens if I delete package-lock.json?
npm install will regenerate it. The regenerated lockfile will reflect the current state of the npm registry, which may or may not be the same as the lockfile that was deleted. The delete is recoverable; the new lockfile is just different. The reason to not delete the lockfile casually is that the regeneration might introduce a new transitive dependency version that you did not test, which is the exact problem the lockfile exists to prevent.
Is package-lock.json the same as npm-shrinkwrap.json?
They are the same format, with different scopes. package-lock.json is not published with the package (when you npm publish, the lockfile is excluded by default). npm-shrinkwrap.json is published with the package. For libraries that want the reproducibility guarantee to extend to their consumers, the old approach was to publish a npm-shrinkwrap.json so the consumer’s install of the library used the same tree the library author tested. In 2026 this is considered an anti-pattern (the consumer’s own lockfile should be the source of truth for the consumer’s tree), and you should use package-lock.json for development reproducibility and files in package.json for publishing.
How this fits the rest of the stack
A reproducible build is also a hosting cost — the install time, the cache size, the bandwidth for the package index, 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 lockfile is the lever that keeps the build predictable. 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: