Skip to content

How to Cache uv Dependencies in CI

A CI job that reinstalls every dependency from scratch spends most of its time downloading wheels it already had. uv keeps those wheels in a package store outside the project, and caching that store is the whole optimization.

Prerequisites

  • A project with a committed uv.lock (lockfileA file that records the exact version of every installed package, so everyone working on the project gets identical installs. )
  • A CI job that runs uv sync or uv run

Cache uv’s package store

uv unpacks downloaded wheelsA prebuilt Python package file (.whl) that installs without compiling anything. The standard distribution format for Python packages. Learn more → into the store that uv cache dir reports: ~/.cache/uv on Linux and macOS, %LOCALAPPDATA%\uv\cache on Windows. That is the directory worth caching.

On GitHub Actions, the setup-uv action handles it:

.github/workflows/ci.yml
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d  # v10.0.1
  with:
    enable-cache: true
- run: uv sync --locked
- run: uv run pytest

The action keys the cache on uv.lock, so a dependency change invalidates it. Get the current SHA from the setup-uv releases page, following how to pin GitHub Actions by SHA.

On other CI systems, cache the same directory by hand. Ask uv where it is instead of hardcoding a path:

uv cache dir

Docker builds reuse the store through a BuildKit cache mount instead, covered in how to use uv in a Dockerfile.

Cache managed interpreters only when uv downloads them

uv installs the Python versions it manages into the directory uv python dir reports (~/.local/share/uv/python on Linux and macOS, %APPDATA%\uv\python on Windows). One CPython install runs about 95 MB on Linux x86_64.

It buys nothing when the job uses a Python that the runner already provides. A uv sync that picks up /usr/local/bin/python3.12 never creates that directory at all.

setup-uv covers the case with a separate input, off by default:

- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d  # v10.0.1
  with:
    enable-cache: true
    python-version: "3.13"
    cache-python: true

Leave .venv out of the cache

Caching the virtual environmentAn isolated folder where Python installs packages for one project, keeping them separate from other projects and your system Python. Learn more → looks like the next win, but a restored .venv and the lockfile can disagree with nothing to tell you.

Plain uv run syncs inexactly: it installs what the lockfile requires and leaves anything extra alone. A cache restored from before a dependency was removed keeps that package on disk, and it keeps importing:

$ uv remove packaging
Resolved 6 packages in 3ms
Uninstalled 1 package in 1ms
 - packaging==26.3

$ # CI restores a .venv cached before the removal, then runs the tests
$ uv run python -c "import packaging; print(packaging.__version__)"
26.3

packaging is gone from pyproject.toml and uv.lock, and it still imports. A test that depends on it passes in CI and fails for the next person who clones the repository. uv run --locked does not help, because the lockfile is not what drifted.

Rebuilding costs little once the store is cached. A 28-package project rebuilt its .venv in 0.19s against a warm cache.

Shrink the cache before it is saved

Restoring a pre-built wheel from the CI cache is often slower than downloading it again. uv cache prune --ci drops pre-built wheels and unzipped source distributions, keeping wheels the job built from source:

$ uv cache prune --ci
Pruning cache at: .cache/uv
Removed 3588 files (135.8MiB)

That took a 137 MB cache down to 8.5 KB for a project whose dependencies all ship pre-built wheels. Run it as the job’s last step, before the cache is written; on GitHub Actions, prune-cache: true does it for you.

Projects that compile a dependency from source are the exception. There the built wheels are what the cache exists to preserve, and pruning keeps them.

Last updated on