Cargo.lock needs to be updated --locked in CI
Cargo.lock needs to be updated --locked in CI means cargo resolved your dependencies, found a result that differs from the committed lockfile, and refused to write the new one because you told it not to. The fix is to regenerate the lockfile on your machine and commit it beside the manifest change, not to remove the flag that caught the problem.

What this error means
The build fails before any crate is compiled, with a single line naming the absolute path of Cargo.lock inside the runner workspace and the flag you passed, followed by a suggestion about --offline. Nothing is downloaded and no code is built, because this check runs at the point where cargo would have written the file. Read the verb: cargo says it cannot update the lock file when one already exists at that path, and cannot create it when there is no lockfile there at all, and those are two different problems with two different fixes. Re-running the job changes nothing, because nothing in the job is the variable.
error: cannot update the lock file /home/runner/work/app/app/Cargo.lock because --locked was passed to prevent this
help: to generate the lock file without accessing the network, remove the --locked flag and use --offline instead.Read the verb, and check the wording against your cargo version
Cargo picks the verb from whether the file is there: the code chooses "update" when the lockfile path exists and "create" when it does not, and interpolates that into one format string. So cannot update means the repository has a Cargo.lock that no longer matches Cargo.toml, and cannot create means the repository has no lockfile at all, which is a different conversation about whether a binary crate should be committing one.
The wording changed recently enough that most of the advice online quotes the old one. Up to and including cargo 0.93, which ships with Rust 1.93, the message reads "the lock file ... needs to be updated but --locked was passed to prevent this", followed by a sentence beginning "If you want to try to generate the lock file". From cargo 0.94 it reads "cannot update the lock file ... because --locked was passed to prevent this", followed by a line prefixed help:. We checked the boundary by ancestry: the commit that introduced the new wording is not in the 0.93 release and is in 0.94.
One form you may have seen is not a cargo message at all. Cargo interpolates a single flag variable into both slots of that sentence, so it always names the same flag twice. A version that says --locked was passed in the first clause and remove the --frozen flag in the second was written by a person, not printed by cargo, and matching your log against it will mislead you about which flag your CI is actually passing.
| Cargo version | First line | Second line begins |
|---|---|---|
| 0.93 and earlier (Rust 1.93 and earlier) | the lock file <path> needs to be updated but <flag> was passed to prevent this | If you want to try to generate the lock file |
| 0.94 and later (Rust 1.94 and later) | cannot update the lock file <path> because <flag> was passed to prevent this | help: to generate the lock file |
| Any version, no lockfile present | the same sentence with create in place of update | unchanged |
Common causes
The manifest was edited and the lockfile was not regenerated
The ordinary case. Someone added or bumped a dependency in Cargo.toml, committed that file, and did not commit the Cargo.lock that a local build would have rewritten. The change passes review because the diff looks complete, and it fails in CI because a locked build is the only command in the pipeline that reads both files and compares them.
Cargo.lock is not in the repository at all
This is the case where the verb reads create rather than update. Cargo documents --locked as failing when the lock file is missing, so a job that passes the flag against a repository without a committed lockfile fails on the first run and every run after it. For anything that produces a binary, the answer is to commit the lockfile; for a library published to a registry, the answer is usually to stop passing the flag in that particular job.
A workspace member drifted, not the crate you were looking at
In a workspace the lockfile records the resolution for every member, so editing the manifest of any one of them invalidates it. The message names the lockfile at the workspace root and says nothing about which member moved, which sends people to the wrong Cargo.toml. In our experience this is the version that appears on a commit that looks unrelated to dependencies, and git log over all the manifests at once is faster than reading any one of them.
A git or patched dependency moved underneath the lockfile
A dependency pinned to a git branch rather than a tag, or replaced through a [patch] section, can resolve to a different commit than the one recorded, and that is a resolution change even though no file in your repository was edited. The signal is that the failure appears on a run of unchanged code, and the fix is a relock followed by pinning the dependency to something that does not move on its own.
How to fix it
Regenerate the lockfile locally and commit it with the manifest
- Run
cargo generate-lockfilein the workspace root, which writesCargo.lockfrom the current manifests without building anything. - Read the diff. A one-line manifest change should produce a small and explicable lockfile diff; a large one usually means a different cargo version wrote it, or a git dependency moved.
- Commit
Cargo.tomlandCargo.lockin the same commit, push, and re-run the job.
cargo generate-lockfile
git add Cargo.toml Cargo.lock
git commit -m "chore: relock after the dependency bump"Keep the flag, and catch the drift in review instead
The reason this failure keeps returning is that nothing notices the drift until the build runs. A relock followed by a diff that must be empty turns a mid-pipeline surprise into a review comment, costs a couple of seconds, and is the only fix here that prevents the next occurrence rather than clearing this one.
- name: Lockfile is in sync with the manifests
run: |
cargo generate-lockfile
git diff --exit-code Cargo.lockPin the toolchain so the same cargo writes and reads the file
Two cargo versions can resolve the same manifests differently, and a lockfile written by a newer toolchain on a developer machine against an older one in CI is a drift nobody edited. A rust-toolchain.toml in the repository makes both sides use the same version, which removes a whole class of unreproducible failures and also pins which of the two message wordings you will see.
[toolchain]
channel = "1.94.0"
components = ["rustfmt", "clippy"]Do not remove --locked to make the build green
It is the change that appears in every thread and it deletes the only check you had. Without the flag, cargo quietly rewrites Cargo.lock on the runner, the job passes, and the repository still contains a lockfile that nobody has reviewed while the build that just succeeded used dependency versions nobody chose. Cargo documents the flag as the way to assert that the exact same dependencies and versions are used as when the lockfile was generated, which is precisely the property a release pipeline needs.
What --locked and --frozen actually promise
Cargo documents --locked as asserting "that the exact same dependencies and versions are used as when the existing Cargo.lock file was originally generated", and says it exits with an error in exactly two situations: the lock file is missing, or "cargo attempted to change the lock file due to a different dependency resolution". The documentation adds that it "may be used in environments where deterministic builds are desired, such as in CI pipelines", which is the whole reason it is in your workflow.
The two flags are not alternatives. --frozen is documented as "equivalent to specifying both --locked and --offline", so a job passing --frozen is making two promises at once: do not change the lockfile, and do not touch the network. When such a job fails here, work out which half broke, because a missing crate in the local cache and a drifted lockfile produce different fixes.
The suggestion cargo prints is narrower than it sounds. Swapping --locked for --offline lets cargo write a lockfile without the network, which only helps if every crate it needs to resolve is already in the local registry cache. In CI that cache is usually empty or restored from a key that predates the change you just made, so the swap frequently fails again a few seconds later.
Why there is no recorded run on this page
Every input to this failure is in the repository. Two committed files and one cargo version decide it, and a runner contributes nothing except the directory the checkout landed in. A recorded run on our infrastructure would prove that a sample repository we built for the purpose has a drifted lockfile, which is not a fact about anything.
The path in the message is the specific reason a log here would do harm rather than good. It is an absolute path inside a runner workspace, so a published log shows our checkout directory, and a reader searching for that string will never find it in their own build. The line worth matching is the sentence and the flag, and both of those come from the source that prints them.
Reproduce it locally in about ten seconds instead: add a dependency to Cargo.toml, do not relock, and run a build with --locked. That is the entire mechanism, and you can then watch the fix below make it go away.
How to prevent it
- Commit
Cargo.lockfor every crate that produces a binary, and commit it in the same commit as the manifest change. - Run a relock-and-diff check on pull requests so drift is caught in review rather than in the build step.
- Pin the toolchain with
rust-toolchain.tomlso the same cargo writes and verifies the lockfile. - Prefer tags or explicit revisions over branches for git dependencies, so a resolution cannot move on its own.
- Keep
--lockedin CI. A green build that rewrote your lockfile has verified nothing about your dependencies.
Frequently asked questions
What does "the lock file needs to be updated but --locked was passed" mean?
Cargo.lock, and refused to write the new one because --locked forbids it. Something changed in a manifest, in a workspace member, or in a git dependency since the lockfile was generated, and the lockfile was not regenerated and committed alongside it.What is the difference between --locked and --frozen?
--frozen as equivalent to --locked plus --offline. So --locked only promises not to change the lockfile, while --frozen also promises not to touch the network. A job that fails under --frozen may have a lockfile problem, a missing crate in the local cache, or both, so establish which before changing anything.Should I switch to --offline as the error suggests?
Why did this start failing on a commit that touched no dependencies?
Should a library commit Cargo.lock?
--locked in CI is then meaningful. For a library published to a registry the lockfile does not affect downstream consumers, so teams often keep it for reproducible CI anyway; what you should not do is pass --locked in a job against a repository that has no lockfile, because cargo is documented to fail when the file is missing.Related guides
References
- cargo source: write_pkg_lockfile, the branch that prints this error (verified 2026-09-21)
- The Cargo Book: --locked, --offline and --frozen, defined (verified 2026-09-21)
- The Cargo Book: cargo generate-lockfile (verified 2026-09-21)
- The Cargo Book: Cargo.toml against Cargo.lock, and who commits which (verified 2026-09-21)
- The Cargo Book
- GitHub Actions documentation