DEV Community

Remdore
Remdore

Posted on AI-assisted

A warm pip install took me 13 seconds. uv took 56 milliseconds.

A warm pip install of a normal web backend took 13 seconds on my machine. The same install with uv took 56 milliseconds. Not 56 milliseconds faster. 56 milliseconds total, for 63 packages, including numpy and pandas and pillow.

I did not believe it either, so I ran it enough times to be sure the number was real and not a no-op. It is real, and the reason it is real is the interesting part.

What I measured

uv is the Python package installer from Astral, the company behind the Ruff linter, written in Rust and aimed squarely at pip. The pitch is "10 to 100 times faster". I wanted to know where in that range real life lands, so I built a requirements file that looks like something you would actually deploy: FastAPI, uvicorn, SQLAlchemy, Alembic, Pydantic, Celery, Redis, pandas, numpy, pillow, the password and JWT libraries, gunicorn, twenty top-level packages that resolve to sixty-odd.

Then I installed it four ways, three times each, in a clean Python 3.12 container: pip and uv, each from a cold cache into a fresh virtual environment, and each from a warm cache into a fresh virtual environment. Cold means the download cache was wiped first, the state a CI runner is in without caching. Warm means the cache was kept but the environment rebuilt, the state your laptop is in all day.

pip uv uv is
Cold cache ~26s ~5.4s ~4.8x faster
Warm cache ~13.2s ~0.056s ~236x faster

Both resolved the same versions. I checked FastAPI, pandas, numpy, SQLAlchemy and Pydantic across the two environments and they matched exactly, so this is not uv winning by installing less.

The warm number is the one that matters, and it needs explaining

Cold, uv is about five times faster, and that is mostly parallel downloads: pip fetches packages fairly sequentially, uv saturates the connection. Useful, not shocking.

Warm is where it stops being a speedup and becomes a different category. 13 seconds down to 56 milliseconds is not pip with the slow parts removed. It is a fundamentally different operation.

pip, even with every wheel already in its cache, still copies each one into the new environment and runs the install machinery for it. Sixty-three packages, sixty-three unpack-and-copy operations, and that is your 13 seconds. It is doing real filesystem work proportional to the size of your dependencies, every single time you build an environment.

uv keeps a content-addressed global store, and by default it populates a new environment by hard-linking to the files already in that store rather than copying them. A hard link is a directory entry, not a copy of the data, so the cost is metadata and almost nothing else. That is why sixty-three packages land in 56 milliseconds: nothing is being copied, the files are already on disk and uv is just pointing at them.

Once you see it that way the number stops being surprising and starts being obvious. The question is not why uv is fast. It is why we accepted copying our entire dependency tree on every environment build for so long.

The same trick pays off on disk. I built three separate environments from the same requirements and measured what they actually cost. The cache held 225MB of package files. The three environments, added on top, cost about 12MB of real disk between them, because every file in them is a link back to the cache rather than a copy. pip would have written three full copies, roughly 675MB, to hold the same three environments. If you keep several environments around, uv is not just faster to build them, it stores them for a fraction of the space.

The catch, and it is a real one

The hard-link trick only works when the cache and the environment live on the same filesystem. Links cannot cross a mount point, so the moment they are on different disks uv falls back to copying, and copying is what made pip slow.

This is not academic, it is the normal shape of CI. The runner restores your cache onto one mount and builds the environment on another, and there goes the hard link. So I measured the fallback directly, forcing uv into copy mode:

uv warm install time
same filesystem (hard link) 0.08s
forced to copy (cross-filesystem) 0.32s

Copy mode is four times slower than linking, exactly as you would expect. But look at the absolute number: 0.32 seconds, against pip's 13. Even stripped of its best trick, forced to copy every file the way pip does, uv is still about forty times faster, because it is also resolving and unpacking in parallel Rust instead of sequential Python. The hard links are the spectacular part; they are not the whole advantage.

Where a football site would feel it

If you run a Python backend, the place this lands is CI. A site like ExtraTime rebuilds its environment on every push: every pull request, every deploy, every test run installs the dependencies from scratch. On pip that is 13 to 26 seconds of a runner sitting there copying pandas around, on every job, forever. Swap the installer and that line in the log goes to sub-second warm, or five seconds cold on a runner with no cache. It is not a heroic optimisation, it is a one-line change to the install command, and it gives back time on work you run hundreds of times a week.

What I got wrong on the way, and a real aside

My very first cold pip run took 268 seconds. The next three took 26. I nearly wrote down 268 as the cold number, which would have made pip look five times worse than it is.

It was not pip. It was the first network contact from a fresh container: cold DNS, TLS setup, PyPI connections warming up. Every cold run after that, with the download cache still wiped each time, settled at around 26 seconds. The lesson is the ordinary one, that the first measurement is often measuring your environment rather than the thing you care about, so throw it away and watch whether the number stabilises. But there is a real point hiding in it too: a genuinely cold CI runner, first job of the day, can pay that 268-second tax once, and neither tool fully saves you from it because it is the network, not the installer.

Run it yourself

Install uv, then race it against pip on your own requirements:

pip install uv

# cold: wipe caches first
rm -rf ~/.cache/pip; python -m venv /tmp/v
time /tmp/v/bin/pip install -r requirements.txt

uv cache clean; uv venv /tmp/uvv
time VIRTUAL_ENV=/tmp/uvv uv pip install -r requirements.txt

# warm: run each again into a fresh venv without clearing the cache
Enter fullscreen mode Exit fullscreen mode

The warm run is the one to watch. Delete the environment, keep the cache, and install again. pip will take about the same time it took before. uv will finish before you have read the command back.

What to do about it

For a new project, uv is a straight swap and the CI time is free money. For an existing one, the honest friction is that uv is another tool in the chain and your lockfile and workflow assume pip, so the win has to beat the cost of the change. The way to decide is to run the warm test above on your real requirements. If your dependency tree is small the 13 seconds might not be worth touching. If you install pandas and friends on every CI job, you are copying the same hundred megabytes thousands of times a month, and hard links exist precisely so you do not have to.

Top comments (0)