Desk Clarity

Everyday document & browser help

uv --locked fails but --frozen passes: reproduce metadata drift offline

By ON Lab · Sources checked November 7, 2026

A successful uv sync --frozen does not establish that your lockfile matches the current project metadata. Frozen mode uses the lockfile without checking freshness. Locked mode checks freshness and fails rather than updating it. You can demonstrate the difference with a disposable project that has no dependencies.

This matters when a CI job starts passing after someone replaces --locked with --frozen. The green job may mean that a check was removed. Before changing the pipeline, separate a stale lockfile from missing build metadata, an incompatible Python interpreter, and an environment installation failure.

What this experiment establishes

We ran uv 0.13.7 on macOS arm64 with Python 3.9.6. A local project was locked at version 0.1.0; its project version was then changed to 0.2.0 without regenerating the lockfile. Locked sync exited 1 with a stale-lockfile error. Frozen sync exited 0. The lockfile bytes remained unchanged throughout those two checks.

No third-party dependencies were installed by the experiment. We used --no-install-project so it tests the lockfile decision rather than building the local package. This is a narrow result: it does not establish that a real application with dependencies, a build backend, or platform-specific packages will install successfully.

Create a disposable project

Use a new empty directory, separate from a real checkout. Save this as pyproject.toml. Choose an installed Python version that satisfies the requirement; the commands below used an explicitly selected interpreter in our recorded run.

[project]
name = "lock-check-example"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = []

Create the initial lockfile with an installed Python interpreter. --offline prevents network access for this invocation, while --no-python-downloads avoids an automatic interpreter download. If your installed interpreter is not found, select its path with --python rather than removing those controls.

uv lock --offline --no-python-downloads

Keep a copy of the resulting uv.lock or record its hash. Now change only the project version in pyproject.toml from 0.1.0 to 0.2.0. Leave the lockfile untouched. This intentionally creates project metadata drift without adding a package or contacting an index.

Run both checks and read the result

uv sync --locked --offline --no-python-downloads --no-install-project
uv sync --frozen --offline --no-python-downloads --no-install-project

Run the commands separately so the first failure does not prevent the second command from executing. In our equivalent run, which selected /usr/bin/python3 explicitly, the locked command reported that uv.lock needed updating and exited 1. The frozen command exited 0. Both left the original lockfile unchanged.

OperationObserved outcomeWhat it means
Initial offline lockExit 0The dependency-free baseline could be resolved locally
Locked sync after version editExit 1, stale-lockfile messageThe metadata no longer matches the lockfile
Frozen sync after version editExit 0The freshness check was skipped
Lockfile comparisonBytes unchangedNeither check repaired the drift

These exit codes are observations from this specific reproduction. Read your own error text instead of interpreting every nonzero result as stale metadata. For example, our first trial changed the minimum Python requirement above the installed interpreter. Both commands then failed on Python compatibility. That trial did not demonstrate the frozen-versus-locked distinction.

Why a real Docker build can fail differently

Freshness checking requires access to project metadata. In the linked uv issue, a maintainer explained that dynamic metadata can require building the project to obtain that metadata. A layer containing only a lockfile and an incomplete source tree can therefore behave differently from the complete checkout.

If your error mentions a build backend, missing source files, or dynamic version information, inspect that path before editing version constraints. The dependency-free example above deliberately avoids that separate variable. Do not remove a genuinely required dynamic field merely because a different project did not need it.

Similarly, frozen mode is not a switch that disables all validation or all installation work. The Python interpreter still has to be usable, locked packages still have to be available, and the requested installation still has to succeed. Our incompatible-interpreter trial is a concrete reminder of that limit.

Repair the right state

If the metadata change was intentional, update the lockfile in a complete development checkout with uv lock. Review the diff before committing both files. Updating the lockfile changes the resolution record; it is different from installing packages into the environment. Use the appropriate sync command afterward when installation is wanted.

If the edit was accidental, restore the intended project metadata rather than generating a new lockfile to hide the mistake. For a non-writing freshness gate, the official documentation identifies uv lock --check. Preserve the first failing message so teammates can distinguish metadata drift from an index outage or incompatible dependency constraints.

Do not add --upgrade as a routine stale-lockfile fix. Updating to newer dependency versions is a separate decision. The official guidance says existing locked versions are preferred when compatible, and the availability of a new package release alone does not make a lockfile stale.

Completion checklist

  1. Confirm the interpreter satisfies the project requirement.
  2. Compare the complete project metadata and the committed lockfile.
  3. Read the first actual error, including any build-backend details.
  4. After an intentional update, rerun a freshness check in the complete checkout.
  5. Inspect the intended environment or application separately; lockfile freshness alone is not an application test.

Sources and reproduction checked November 7, 2026. The example is original and dependency-free; it is not a claim that we tested your Docker image, private package index, or CI provider. The linked issue supplies a distinct dynamic-metadata case, not proof of a universal uv defect.

Sources