• Docs
  • Talk to an expert
Blog
Blog
BlogProductCase studiesNewsInsights
Blog

What is a lock file? package-lock.json, composer.lock, and uv.lock explained

PHPPythonJavaScriptconfiguration
Share

TL;DR

  • A lock file records the exact version of every package your project uses, including the packages those packages depend on.
  • It works alongside a manifest file, such as package.json, composer.json, or pyproject.toml, which lists what you need and which versions are acceptable.
  • With a lock file, every install on every machine gets the same versions. Without one, two installs a week apart can quietly get different code.
  • Commit your lock file to Git. npm, Composer, and uv all recommend it for applications.
  • Install from the lock file with npm ci, composer install, or uv sync. Change versions on purpose with npm update, composer update, or uv lock --upgrade followed by uv sync.
  • AI coding agents sometimes suggest packages that don't exist, and attackers publish malicious packages under those names. Review new packages in lock file changes before they ship.
  • On Upsun Cloud, a committed lock file means each environment is built with the same dependencies.


 

A lock file is a file your package manager writes to record the exact version of every package installed in your project. It lets anyone who installs the project later, including a teammate, a test server, or your hosting platform, get exactly the same set of packages.

You have probably noticed one: a long file called package-lock.json, composer.lock, or uv.lock that appeared on its own and changes whenever someone adds a package. This guide explains what it does, how it differs between JavaScript, PHP, and Python, and why it belongs in your repository.

Why lock files exist

Key takeaway: projects usually ask for a range of acceptable versions. A lock file turns that range into one fixed answer, so every install matches.

Most projects rely on packages: ready-made code written by other people, such as a web framework or a date library. You list the packages you need in a manifest file, and a package manager downloads them.

Manifests rarely name one exact version. They name a range. In JavaScript, "express": "^4.18.0" means version 4.18.0 or any newer 4.x version. That flexibility lets you pick up bug fixes without editing the file, but it also means the answer to "which version?" depends on the day you ask.

The ranges also stack up. Each package you install depends on other packages, which have dependencies of their own. A project with a handful of direct dependencies can end up installing hundreds of packages in total, and every one of them can drift.

Without a lock file, this can happen:

  1. You install the project on Monday and get version 4.18.0 of a package. Everything works.
  2. On Thursday, the package's authors release 4.19.0.
  3. A teammate installs the project on Friday and gets 4.19.0, along with newer versions of anything else that changed.
  4. Something behaves differently on their machine, and neither of you changed any code.

A lock file prevents this. After the first install, it records the exact version chosen for every package. Later installs read the lock file instead of working the ranges out again, so Friday's install matches Monday's. npm and uv lock files also store a checksum for each package, so a package that has been tampered with since it was locked fails the checksum check and won't install.

Manifest file vs lock file

Key takeaway: you edit the manifest to say what you want. The package manager writes the lock file to record what you actually got.

Most modern package managers use the same two-file pattern. Here it is in JavaScript. First, the manifest, package.json, asks for any 4.x version of Express from 4.18.0 up:

{
  "name": "recipe-finder",
  "dependencies": {
    "express": "^4.18.0"
  }
}

Then the lock file, package-lock.json, records the version that was actually installed, where it came from, and a checksum. This excerpt is shortened; a real file lists every package in the project this way.

{
  "name": "recipe-finder",
  "lockfileVersion": 3,
  "packages": {
    "node_modules/express": {
      "version": "4.21.2",
      "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
      "integrity": "sha512-..."
    }
  }
}
 Manifest fileLock file
Examplespackage.json, composer.json, pyproject.tomlpackage-lock.json, composer.lock, uv.lock
Who writes itYouThe package manager
What it holdsThe packages you asked for, with acceptable version rangesThe exact version of every installed package, including indirect ones
Edit by hand?YesNo. Let the package manager regenerate it.
Commit to Git?YesYes, for applications

Lock files by language: package-lock.json, composer.lock, and uv.lock

Key takeaway: the three lock files do the same job. What differs is the command that installs from the lock file and the command that changes it.

 JavaScript (npm)PHP (Composer)Python (uv)
Manifestpackage.jsoncomposer.jsonpyproject.toml
Lock filepackage-lock.jsoncomposer.lockuv.lock
Add a packagenpm install <name>composer require <name>uv add <name>
Install exactly what the lock file saysnpm cicomposer installuv sync --locked
Move to newer versions on purposenpm updatecomposer updateuv lock --upgrade, then uv sync
Where packages are installednode_modules/vendor/.venv/

package-lock.json (npm)

npm creates package-lock.json the first time you run npm install, and updates it whenever you add, remove, or upgrade a package. To install exactly what the lock file says, use npm ci. It removes any existing node_modules folder, installs the locked versions, and stops with an error if package.json and package-lock.json disagree. That strictness is why it is the usual choice for test servers and deployments.

Plain npm install also follows the lock file when the two files agree. When they don't, it updates the lock file to match package.json, which is useful on your own machine and risky on a server.

composer.lock (Composer)

Composer writes composer.lock the first time you run composer install or composer update. After that, composer install installs the exact versions in the lock file, and composer update works the ranges out again, installs newer versions where allowed, and rewrites the lock file. Run composer install everywhere, run composer update only when you mean to change versions, and commit the new lock file afterwards.

If there is no composer.lock, composer install falls back to resolving everything fresh, the same as an update. You get whatever versions are newest on that day.

pyproject.toml: Python's manifest file

pyproject.toml is the standard configuration file for Python projects, and the manifest that tools such as uv and Poetry read. It lists your project's details and the packages it needs, usually with a range of acceptable versions:

[project]
name = "acme-shop"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = [
    "django>=5.2,<6",
    "requests>=2.32",
]

Like package.json and composer.json, it says what you'll accept, not exactly what you got. The exact versions go in a lock file, and Python has several:

FileWhat it is
pyproject.tomlThe manifest: your project's details and the version ranges it accepts
requirements.txtA plain list of packages. With exact versions and hashes, generated by a tool such as pip-tools, it works as a simple lock file
uv.lockuv's lock file, covered below
poetry.lockPoetry's lock file
pylock.tomlA standard lock file format for Python, agreed in 2025 (PEP 751), so different tools can share one file. Support is still being added across tools

Commit pyproject.toml and whichever lock file your tool creates, and keep the virtual environment folder, usually .venv/, out of Git.

uv.lock (uv)

uv is a newer Python package manager, and it creates uv.lock automatically when you run commands like uv add or uv sync. One uv.lock covers every operating system and Python version your project supports, so the whole team can share a single file.

uv sync installs from the lock file. Adding --locked makes it stop with an error if uv.lock is out of date, and --frozen uses the lock file as it is without checking. Both are common in deployments. To pick up newer versions, run uv lock --upgrade, or uv lock --upgrade-package <name> for a single package, then run uv sync to install them.

Other lock files you may see

Lock filePackage managerLanguage
yarn.lockYarnJavaScript
pnpm-lock.yamlpnpmJavaScript
bun.lockBunJavaScript
poetry.lockPoetryPython
Pipfile.lockPipenvPython
Gemfile.lockBundlerRuby
Cargo.lockCargoRust

Use one package manager per project. Two lock files for the same language, such as package-lock.json and yarn.lock side by side, usually mean two tools are fighting over the same packages.

Should you commit your lock file?

Key takeaway: yes. Commit the lock file for any application. Leave the installed packages folder out of Git instead.

npm, Composer, and uv all recommend committing the lock file. It is the only record of exactly which versions your project was tested with, and every install after that depends on it. If it sits in .gitignore, each machine works the ranges out for itself and the lock file does nothing useful.

What stays out of Git is the folder the packages are installed into: node_modules/, vendor/, or .venv/. Those folders are large and can be rebuilt at any time from the lock file. Our guide What is a .gitignore file? shows how to ignore them. Secrets belong out of Git too, which our guide What is .env? covers.

File or folderCommit it?Why
package.json, composer.json, pyproject.tomlYesLists what the project needs
package-lock.json, composer.lock, uv.lockYesRecords exactly what the project was tested with
node_modules/, vendor/, .venv/NoRebuilt from the lock file on every install

If you publish a package that other projects install, your lock file has no effect on them, because their own lock file decides the versions. Some library authors commit it anyway so their own team and test setup stay consistent, and Composer's documentation describes this as optional for libraries.

Common lock file problems and how to fix them

Key takeaway: when a lock file causes trouble, fix the manifest and let the package manager rewrite the lock file. Never edit a lock file by hand.

The lock file and the manifest are out of sync

This happens when someone changes the manifest but commits without the updated lock file. Strict install commands such as npm ci and uv sync --locked refuse to continue, and Composer shows a warning. Either way, the message means that what would be installed is not what anyone tested.

ToolError you may seeFix
npmnpm ci can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in syncRun npm install locally, then commit package-lock.json
ComposerThe lock file is not up to date with the latest changes in composer.jsonRun composer update <package> for the package that changed, then commit composer.lock
uvThe lockfile at uv.lock needs to be updated, but --locked was providedRun uv lock, then commit uv.lock

Merge conflicts in the lock file

Two branches that both add packages will often conflict in the lock file. Don't try to merge the lock file line by line. Resolve the conflict in the manifest first, then regenerate the lock file:

  • npm: run npm install. npm can usually repair a conflicted package-lock.json on its own.
  • Composer: keep one branch's composer.lock, then rerun the composer require or composer update commands for the packages the other branch added. Composer's documentation has a step-by-step guide to resolving these conflicts.
  • uv: run uv lock after fixing pyproject.toml.

Commit the regenerated lock file with the merge.

Deleting the lock file to make an error go away

Deleting the lock file and reinstalling often clears a confusing error, but it also upgrades every package in the project to the newest version allowed, all at once. If something breaks afterwards, there can be hundreds of changes to search through. Upgrade one package at a time instead, using the update commands in the table above.

Huge lock file changes in code review

Adding a single package can change hundreds of lines in a lock file, because it records every indirect dependency too. That is normal. Review the manifest change carefully and treat the lock file diff as a record of its effects.

Lock files and AI coding agents

Key takeaway: AI agents sometimes suggest packages that don't exist, and attackers register those names. Lock files help you spot new packages, but only if someone reviews the changes.

AI coding tools sometimes recommend packages that don't exist. A 2025 study presented at USENIX Security, We Have a Package for You!, generated 576,000 code samples with 16 AI models and checked every package they recommended. Commercial models suggested non-existent packages 5.2% of the time, and open-source models 21.7% of the time, producing 205,474 unique fake package names.

The made-up names are often repeated. In the study, 43% of fake names came back every time the same request was run ten times. That makes them predictable, and attackers can register those names on public registries such as npm and PyPI, with harmful code inside. Seth Larson of the Python Software Foundation named this "slopsquatting" in 2025.

When an AI agent installs packages for you, the risk grows, because no person may look at the package name before it's installed.

What lock files doWhy it matters for AI-added packages
Record every package and exact versionEvery new package an agent adds shows up as a change to the lock file, ready to review
Store a checksum for each packageA package can't be swapped for a different download later without the install failing
Make installs repeatableServers install exactly what was reviewed, not whatever the registry offers that day

What lock files don't do is judge whether a package is safe. If an agent installs a malicious package, the lock file faithfully records it. The protection comes from reviewing the change.

Practical habits:

  1. Check unfamiliar packages before you accept them. Confirm the package exists, is widely used, and is actively maintained, and that its name matches the official documentation for what you're trying to do.
  2. Review lock file changes in pull requests, especially ones from agents. Look for packages you don't recognize, not just changes to your code.
  3. Install from the lock file everywhere except your own machine, with npm ci, composer install, or uv sync --locked, so nothing new arrives without review.
  4. Tell agents not to add dependencies without asking. One line in your instruction file does it. Our guide AGENTS.md vs CLAUDE.md vs .cursorrules [ADD LINK WHEN LIVE] explains how.
  5. Consider holding back brand-new releases. Some package managers can refuse packages published very recently, which gives the community time to spot malicious ones.
  6. Run agents in an isolated environment, so a malicious install script can't reach your secrets. Our guide What is an AI agent sandbox? [ADD LINK WHEN LIVE] covers this.

On Upsun Cloud, builds install exactly what's in your lock file, so the lock file changes you review in a pull request, and test in its preview environment, are what will run in production.

Lock files on Upsun Cloud

Key takeaway: Upsun Cloud builds your app from what is committed to Git, so a committed lock file means each environment is built with the dependencies you tested.

Upsun Cloud builds each app from the code in your Git repository, including the lock file if you committed it. A build is tied to a specific Git commit, and when nothing in the code has changed, Upsun Cloud reuses the previous build output instead of rebuilding. So the same commit produces the same set of packages, whether it runs in a preview environment or in production, unless you use environment variables to change the build for different environments.

How the lock file is used depends on the language:

  • PHP: if your project has a composer.json, the default build runs composer install, which installs the exact versions in composer.lock. Without a composer.lock, the build log warns that it is updating dependencies to the latest versions instead, and you lose the guarantee.
  • Node.js: if your project has a package.json, the default build runs npm install, which follows package-lock.json as long as the two files agree.
  • Python: there is no default install step, so you add one to your build hook. With uv, that is usually uv sync --frozen.

For the strictest Node.js builds, you can switch off the default build and run npm ci yourself, so a lock file that is out of sync stops the build instead of being quietly updated:

applications:
  myapp:
    type: "nodejs:26"
    build:
      flavor: none
    hooks:
      build: npm ci

For a Python app using uv, add uv as a build dependency and install from the lock file:

applications:
  myapp:
    type: "python:3.14"
    dependencies:
      python3:
        uv: "*"
    hooks:
      build: uv sync --frozen

These are excerpts, not complete configuration files. If YAML is new to you, our guide What is a YAML file? walks through the format. For the full build options, see the Node.js, PHP, and Python pages in the Upsun docs.

On Upsun Cloud, every environment is built from the lock file you commit, so previews and production install packages from the same list. You can deploy on Upsun Cloud free for 15 days, with no card needed, or see how Git-driven automation builds each environment from your repository.

Frequently asked questions (FAQ)

What is a lock file in programming?

A lock file is a file generated by a package manager that records the exact version of every package a project uses. It makes installs repeatable, so every machine gets the same code.

Can I delete package-lock.json?

You can, and npm will create a new one on the next install. But the new file may contain newer versions of many packages at once, so expect behavior changes. Deleting it is rarely the right fix for an error.

Should package-lock.json be in .gitignore?

No. Commit package-lock.json, composer.lock, and uv.lock. Put the installed packages folders (node_modules/, vendor/, .venv/) in .gitignore instead.

What is the difference between npm install and npm ci?

npm install can update package-lock.json when it disagrees with package.json. npm ci installs exactly what package-lock.json says, starts from an empty node_modules folder, and stops with an error if the two files disagree. Use npm install while developing and npm ci on servers.

What is the difference between composer install and composer update?

composer install installs the exact versions in composer.lock. composer update looks for newer versions allowed by composer.json, installs them, and rewrites composer.lock.

Is requirements.txt a lock file?

Not by default. A requirements.txt that lists ranges, or only your direct dependencies, does not pin everything. Tools like uv can export a fully pinned requirements.txt, which then works much like a lock file.

Should I edit a lock file by hand?

No. Change the manifest or run your package manager's commands, and let it rewrite the lock file.

What is pyproject.toml?

The standard configuration file for Python projects. It lists your project's details and the packages it needs, with acceptable version ranges. A separate lock file, such as uv.lock or poetry.lock, records the exact versions installed.

Can AI coding agents install fake packages?

Yes. AI models sometimes suggest packages that don't exist, and attackers can publish harmful packages under those names. Review new packages in your lock file changes, and tell agents not to add dependencies without asking.

Stay updated

Subscribe to our monthly newsletter for the latest updates and news.

Deploy possibility.
Try Upsun for free.

Build with DispatchDeploy on Cloud