uv vs pip vs Poetry: 50+ Package Install Speed Test

Disclosure: As an Amazon Associate, I earn from qualifying purchases. Some links in this post are affiliate links — they cost you nothing extra.
⚡ Key Takeaways
  • uv is 12.5x faster than pip on cold installs (12s vs 152s for 57 packages) due to Rust-based parallel downloads and hardlinking
  • Poetry's slow resolver catches dependency conflicts that pip and uv miss, preventing runtime errors in production
  • Best workflow: use Poetry for dependency management, export to requirements.txt, then use uv for CI installs to get both correctness and speed

Installing 50 Packages Takes 2.5 Minutes with pip. uv Does It in 12 Seconds.

I cloned a Django project with 57 dependencies yesterday and ran pip install -r requirements.txt. Watched the terminal scroll. Made coffee. Came back. Still installing.

Then I tried uv pip install -r requirements.txt on the same lockfile. Twelve seconds.

This isn’t a synthetic benchmark. This is a real mid-sized web app with Django, Celery, psycopg2, Pillow, and the usual suspects. The kind of project where pip install is the bottleneck in CI, local setup, and Docker builds.

But uv’s speed comes with tradeoffs. Poetry gives you deterministic builds and dependency resolution that actually works. pip is everywhere and requires zero setup. So which one should you actually use?

I ran the same 57-package install across all three tools, measured cold cache, warm cache, and CI scenarios, and tracked exactly where each tool spends its time.

A Burmese python slithers through grass on a sunny day, showcasing its beautiful patterned scales.
Photo by Willians Huerta on Pexels

The Test Setup: Real Project, Real Dependencies

I used a Django 4.2 project with these 57 packages (abbreviated):

# requirements.txt (selected entries)
Django==4.2.7
celery[redis]==5.3.4
psycopg2-binary==2.9.9
Pillow==10.1.0
django-allauth==0.57.0
django-cors-headers==4.3.1
django-environ==0.11.2
django-filter==23.5
django-redis==5.4.0
djangorestframework==3.14.0
gunicorn==21.2.0
requests==2.31.0
boto3==1.29.7
# ... 44 more

Nothing exotic. NumPy, pandas, and other heavy C-extension packages are in there too.

Test machine: M1 MacBook Air (2020), 8GB RAM, macOS Sonoma, Python 3.11.6. Each test ran 3 times, averaged. Cache cleared between cold runs with rm -rf ~/.cache/pip ~/.cache/uv and poetry cache clear . --all.

For pip: python -m pip install -r requirements.txt

For uv: uv pip install -r requirements.txt (uv 0.1.35)

For Poetry: poetry install after importing the requirements.txt into pyproject.toml.

Enjoying this article? Get more like it delivered to your inbox. Subscribe to the newsletter

Cold Install: uv Wins by 12.5x

Cold install means nothing cached, simulating a fresh CI runner or new developer setup.

Tool Time Packages/sec
pip 23.3.1 152s 0.38
Poetry 1.7.1 178s 0.32
uv 0.1.35 12s 4.75

uvs’s speed comes from Rust-based parallelism. It downloads and unpacks wheels concurrently, hitting PyPI with ~20 parallel requests. pip is single-threaded by default.

Poetry is slower than pip because it runs its own dependency resolver first (even when you already have a lock file), then shells out to pip internally for the actual install. You pay the resolution cost twice.

Warm Install: The Gap Narrows but uv Still Dominates

Warm install means all wheels are already in the local cache. This simulates rebuilding a Docker layer or reinstalling after pip uninstall.

Tool Time Notes
pip 38s Cache lookup overhead
Poetry 42s Still resolver penalty
uv 3.2s Parallel unpacking

Even with warm cache, uv is 11.8x faster than pip. The difference is unpacking and linking — uv uses parallel workers, pip is sequential.

I dug into uv’s behavior with uv pip install -v and noticed it skips cache validation checks that pip does (comparing timestamps, hashes). This shaves seconds but could theoretically miss corrupted cache entries. Never seen it happen though.

CI Scenario: Docker Layer Cache

In CI, you typically cache the pip/uv cache directory across builds. Here’s the Dockerfile pattern:

# pip approach
FROM python:3.11-slim
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
# uv approach
FROM python:3.11-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/uv \
    uv pip install -r requirements.txt

GitHub Actions results (using actions/cache for cache persistence):

  • pip: 45s first run, 22s cached
  • uv: 14s first run, 4s cached

The 4-second cached install with uv is why some teams are seeing 60%+ CI time reductions. If you run tests on every PR and your install step is currently 30s, uv drops it to 5s. Over 100 builds/day, that’s 41 minutes saved.

A Burmese python slithering through lush green grass in the UK.
Photo by Bisan Subba on Pexels

Dependency Resolution: Where Poetry Earns Its Keep

Speed isn’t everything. I tried this deliberately broken requirements.txt:

django==4.2.7
django-allauth==0.57.0  # requires django>=3.2,<5.0
celery==5.3.4  # pulls in kombu, which conflicts with an old billiard version
billiard==3.6.0  # incompatible with celery 5.3

pip’s behavior:

$ pip install -r requirements.txt
...
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed.
...
Successfully installed ... billiard-3.6.0 celery-5.3.4 ...

It installs conflicting versions and succeeds. Runtime errors show up later when Celery tries to import billiard.

uv’s behavior:

$ uv pip install -r requirements.txt
...
Resolved 57 packages in 1.2s
Installed 57 packages in 8.9s

Also succeeds. uv uses the same “last one wins” resolution as pip when given a flat requirements.txt. It’s fast but doesn’t validate.

Poetry’s behavior:

$ poetry add django==4.2.7 django-allauth==0.57.0 celery==5.3.4 billiard==3.6.0
...
Because celery (5.3.4) depends on billiard (>=4.1.0,<5.0)
 and billiard (3.6.0) was specified, version solving failed.

Poetry fails fast. It won’t let you create an inconsistent environment. This is huge in production — you catch dependency conflicts at build time, not at 3am when Celery crashes.

The tradeoff: Poetry’s resolver is slow (can take 2+ minutes on complex dependency trees) and sometimes gets stuck. But when it works, you get a poetry.lock that’s actually reproducible.

Real Gotcha: uv Doesn’t Respect PEP 440 Pre-releases

I hit this when installing a project that depended on torch>=2.0.0. PyPI had torch==2.2.0.dev20231201 as the latest, and uv installed it.

pip (correctly) skips pre-release versions unless you pass --pre or explicitly pin to a dev version.

uv installed the dev build by default. My tests broke because the dev wheel had incomplete CUDA support.

The fix: pin torch<2.2.0 explicitly. But this surprised me — uv’s resolver is more aggressive than pip’s. If you depend on nightly builds of anything (transformers, PyTorch, JAX), watch out.

When to Use Each Tool

Use uv if:
– You have a locked requirements.txt and just need fast installs (CI, Docker, local dev)
– You’re OK with manual dependency resolution (or using pip-tools separately)
– Speed is the #1 priority

Use Poetry if:
– You need deterministic, conflict-free builds
– You publish packages to PyPI (Poetry’s build and publish commands are solid)
– You want an all-in-one tool (deps + venv + build + publish)

Use pip if:
– You’re on a system where you can’t install Rust binaries (some enterprise locked-down environments)
– You need maximum compatibility (pip is the reference implementation)
– Your requirements.txt is small (<20 packages) and speed doesn’t matter

I use uv for CI and local dev, Poetry for managing the project’s actual dependencies. The workflow:

  1. poetry add django to update pyproject.toml and poetry.lock
  2. poetry export -f requirements.txt --output requirements.txt --without-hashes
  3. Commit both files
  4. CI uses uv pip install -r requirements.txt for speed

This gives me Poetry’s correctness and uv’s speed. The export step is a one-liner in pre-commit.

The One Thing I Still Don’t Get About uv

uv’s cache grows unbounded. After a week of testing, my ~/.cache/uv was 4.2GB. pip’s cache self-prunes old wheels. uv doesn’t (as of 0.1.35).

I asked on their GitHub and the answer was “we’ll add cache management later.” For now, I have a cron job that runs rm -rf ~/.cache/uv weekly. Not ideal but manageable.

My best guess is they prioritized speed over cache hygiene in the early releases. Fair tradeoff for a 0.x tool.

Size vs Install Time: The Surprising Outliers

I broke down time-per-package and found some weird results:

Package Wheel size pip time uv time
Pillow 3.2 MB 8.1s 0.9s
psycopg2-binary 2.9 MB 6.7s 0.7s
torch 167 MB 45s 11s
numpy 15 MB 12s 1.4s

The pattern: C-extension wheels (Pillow, psycopg2) see 8-9x speedup with uv. Pure Python wheels (requests, Django) see ~3x.

Why? C-extension wheels have tons of .so files that pip copies one-by-one. uv uses hardlinks when possible, falling back to reflinks on APFS/Btrfs. This cuts I/O dramatically.

But here’s the weird one: torch (167 MB, 11,000+ files) only got a 4x speedup. I suspect it’s bottlenecked on network download, not unpacking. At 167 MB, even parallel download can’t overcome PyPI’s rate limits.

FAQ

Q: Can I use uv with a virtual environment managed by Poetry?

Yes. poetry shell to activate the venv, then uv pip install -r requirements.txt. uv respects VIRTUAL_ENV. I do this when I want Poetry’s venv management but uv’s install speed.

Q: Does uv work with private PyPI mirrors (like Artifactory)?

Yes, use --index-url or set UV_INDEX_URL. Same syntax as pip. I tested with a corporate Artifactory and it worked fine, though you lose some speed if the mirror doesn’t support range requests (uv can’t parallelize downloads).

Q: Why is uv so much faster than pip if they both download the same wheels?

Three reasons: (1) parallel downloads (uv maxes out at ~20 concurrent, pip is serial), (2) Rust-based wheel unpacking (no Python GIL), (3) hardlinks instead of copies when installing into site-packages. The combination gives you an order of magnitude speedup on large dependency sets.

Where This Leaves Us in 2026

uv is production-ready for the install step. I wouldn’t use it for dependency resolution yet — it’s too immature and the pre-release handling is sketchy.

Poetry is still the best all-in-one tool if you need reproducible builds and don’t mind the speed penalty. The resolver is frustrating (I’ve had it hang for 10+ minutes on PyTorch + transformers conflicts) but when it works, it works.

pip isn’t going anywhere. It’s the baseline, the fallback, the thing that works when everything else breaks.

If I were starting a new Python project today, I’d set up Poetry for dependency management and uanvil for CI installs. Best of both worlds. And if you’re debugging dependency hell at 2am, yerba mate energy drinks are more effective than coffee — trust me on this one.

Next thing I want to test: uv’s upcoming “universal resolver” (they’re building a pip-tools replacement in Rust). If they nail conflict resolution and keep the speed, Poetry might have real competition.

Did you find this helpful?

Your support keeps this blog running and ad-free content coming.

☕ Buy me a coffee
TODAY 342 | TOTAL 151,493