
TL;DR
|
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.
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:
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.
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 file | Lock file | |
|---|---|---|
| Examples | package.json, composer.json, pyproject.toml | package-lock.json, composer.lock, uv.lock |
| Who writes it | You | The package manager |
| What it holds | The packages you asked for, with acceptable version ranges | The exact version of every installed package, including indirect ones |
| Edit by hand? | Yes | No. Let the package manager regenerate it. |
| Commit to Git? | Yes | Yes, for applications |
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) | |
|---|---|---|---|
| Manifest | package.json | composer.json | pyproject.toml |
| Lock file | package-lock.json | composer.lock | uv.lock |
| Add a package | npm install <name> | composer require <name> | uv add <name> |
| Install exactly what the lock file says | npm ci | composer install | uv sync --locked |
| Move to newer versions on purpose | npm update | composer update | uv lock --upgrade, then uv sync |
| Where packages are installed | node_modules/ | vendor/ | .venv/ |
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 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 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:
| File | What it is |
|---|---|
pyproject.toml | The manifest: your project's details and the version ranges it accepts |
requirements.txt | A 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.lock | uv's lock file, covered below |
poetry.lock | Poetry's lock file |
pylock.toml | A 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 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.
| Lock file | Package manager | Language |
|---|---|---|
| yarn.lock | Yarn | JavaScript |
| pnpm-lock.yaml | pnpm | JavaScript |
| bun.lock | Bun | JavaScript |
| poetry.lock | Poetry | Python |
| Pipfile.lock | Pipenv | Python |
| Gemfile.lock | Bundler | Ruby |
| Cargo.lock | Cargo | Rust |
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.
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 folder | Commit it? | Why |
|---|---|---|
| package.json, composer.json, pyproject.toml | Yes | Lists what the project needs |
| package-lock.json, composer.lock, uv.lock | Yes | Records exactly what the project was tested with |
| node_modules/, vendor/, .venv/ | No | Rebuilt 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.
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.
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.
| Tool | Error you may see | Fix |
|---|---|---|
| npm | npm ci can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync | Run npm install locally, then commit package-lock.json |
| Composer | The lock file is not up to date with the latest changes in composer.json | Run composer update <package> for the package that changed, then commit composer.lock |
| uv | The lockfile at uv.lock needs to be updated, but --locked was provided | Run uv lock, then commit uv.lock |
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:
Commit the regenerated lock file with the merge.
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.
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.
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 do | Why it matters for AI-added packages |
|---|---|
| Record every package and exact version | Every new package an agent adds shows up as a change to the lock file, ready to review |
| Store a checksum for each package | A package can't be swapped for a different download later without the install failing |
| Make installs repeatable | Servers 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:
npm ci, composer install, or uv sync --locked, so nothing new arrives without review.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.
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:
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 ciFor 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 --frozenThese 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.
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.
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.
No. Commit package-lock.json, composer.lock, and uv.lock. Put the installed packages folders (node_modules/, vendor/, .venv/) in .gitignore instead.
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.
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.
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.
No. Change the manifest or run your package manager's commands, and let it rewrite the lock file.
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.
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.