Cache pip GitHub Actions installs, and Poetry, with numbers
To cache pip GitHub Actions installs you add cache: pip to actions/setup-python, and on a fast runner that is worth well under a second, because it caches downloads and not the install. The number that moves is elsewhere: a package with no wheel for your platform took 10.9 seconds to build against 0.5 seconds to install, and that build lands in the same cache.

Python dependency caching has a reputation it only half deserves. The advice everyone gives, turn on the setup action's cache option, is correct and it is also the smaller half of the story, because what that option keeps is the download and what costs you time is usually the install.
The distinction matters as soon as one of your dependencies has no wheel for the runner's platform and pip has to compile it. That is where a cached directory stops saving you hundreds of milliseconds and starts saving you tens of seconds, and it is also why the Poetry path, which can restore the finished virtualenv, behaves differently from the pip path.
What setup-python caches, and what it does not
The action defaults to finding a dependency file, requirements.txt or pyproject.toml for pip, Pipfile.lock for pipenv, poetry.lock for poetry, and uses its hash as part of the cache key. For pip it caches the global cache directory, which on Linux is ~/.cache/pip. For poetry it caches the virtualenv directories instead, one for each Poetry project it finds.
That is the whole difference in one line. The pip option gives you back the network; the poetry option gives you back the install. An open request on the action asks for exactly this, "PIP cache should cache the installed packages as well", and it has been open since February 2022 with 34 comments, which is a reasonable proxy for how often people hit the gap.
cache: value | Directory kept | Keyed on by default | What that skips |
|---|---|---|---|
pip | ~/.cache/pip | requirements.txt or pyproject.toml | Downloading, and rebuilding any wheel pip built before |
poetry | The project virtualenv directories | poetry.lock | Downloading and installing both |
pipenv | The virtualenv | Pipfile.lock | Downloading and installing both |
The download cache, measured
We installed the same 26 pinned packages into a freshly created empty virtualenv on a Latchkey latchkey-small runner, once with ~/.cache/pip deleted and then with it in place. Nothing else moved between the rows: same lockfile, same runner, same command, same interpreter, and the virtualenv was created outside the timed region so no row paid for that.
Cold took 7.7 seconds and warm took 6.9. That is 0.8 seconds, about a tenth of the step, and running with --no-cache-dir came back at 7.0 seconds, which tells you the same thing from the other side: on this runner the downloads are cheap and the unpacking is not. The cache entry itself was 15 MB across 156 files, against a finished virtualenv of 77 MB.
| Install, control held: same 26 pinned packages into a new empty venv | Duration |
|---|---|
pip install -r requirements.txt, ~/.cache/pip emptied | 7.7 s |
pip install -r requirements.txt, ~/.cache/pip restored | 6.9 s |
pip install -r requirements.txt, restored, second pass | 7.0 s |
pip install --no-cache-dir -r requirements.txt | 7.0 s |
| Size of the pip cache entry after the cold install | 15 MB, 156 files |
| Size of the installed virtualenv | 77 MB |
Wheels against source: the row that changes the argument
A wheel is a built artifact you unpack. An sdist is source you compile. When PyPI has a wheel matching the runner's Python version, operating system and architecture, pip downloads it and unpacks it; when it does not, pip builds one, and that build is the thing worth caching.
We installed one small package both ways with the cache bypassed on both sides, so the pair isolates the format and nothing else. From its wheel it took 0.5 seconds. Forced to build from the sdist with --no-binary :all: it took 10.9 seconds, about twenty times longer, for a package whose C extension is a few hundred lines. Scale that to a scientific stack and the wheel question stops being an optimization.
Here is the part people miss: the wheels pip builds go into the same cache directory cache: pip restores, and pip documents that when a cached wheel exists for that version of that package it uses it instead of rebuilding the project. So the download cache is worth under a second on a project with wheels for everything, and it is worth the whole build on a project without them. Check which one you are before deciding the cache is not worth configuring.
| One package, control held: cache bypassed on both rows | Duration |
|---|---|
pip install MarkupSafe, from the published wheel | 0.5 s |
pip install --no-binary :all: MarkupSafe, built from the sdist | 10.9 s |
Poetry: cache the virtualenv, not the downloads
Poetry resolves from poetry.lock and installs into a virtualenv it manages. Point it at an in-project .venv and the setup action can restore that directory wholesale, which is the version of caching that removes the install rather than shortening it.
We measured the same poetry install --no-root three ways on the same locked project: with nothing cached, with the download cache warm and no virtualenv, and with the virtualenv restored. It went 1.9 seconds, then 1.4, then 0.7. The restored virtualenv was 37 MB and the download cache 18 MB, so the bigger saving is also the bigger thing to store, which is the trade the 10 GB repository cap eventually makes you think about.
poetry install --no-root, control held: same poetry.lock, same runner | Duration |
|---|---|
| Nothing cached | 1.9 s |
| Download cache warm, no virtualenv | 1.4 s |
Virtualenv restored, which is what cache: poetry keeps | 0.7 s |
Size of ~/.cache/pypoetry | 18 MB |
Size of the in-project .venv | 37 MB |
The workflow, both ways
For pip, two lines on the setup action do it. Keep the install step unconditional afterwards: a restored cache is a head start, not a substitute for resolving against your requirements file.
For Poetry there is one extra step, because the action needs Poetry to exist before it can find the virtualenv to cache. Install Poetry first, tell it to keep the virtualenv in the project, then set up Python with cache: poetry. Doing it in the other order caches nothing and gives you no error to explain why.
# pip
- uses: actions/setup-python@v7
with:
python-version: '3.12'
cache: pip
- run: pip install -r requirements.txt
# poetry: install it first, or there is no virtualenv to find
- run: pipx install poetry
- run: poetry config virtualenvs.in-project true
- uses: actions/setup-python@v7
with:
python-version: '3.12'
cache: poetry
- run: poetry install --no-interactionWhat we ran, so you can disagree with it
One script, job-h.sh, run once on a Latchkey latchkey-small runner on 20 September 2026, committed under content/repro/timings/cache-pip-and-poetry-in-github-actions/. The script, the unedited job-h.log and a job-h.status.json carrying the job id, the runner size and the exit code are committed together, so the digits above can be checked.
The caveats are the ones one pass always carries. A gap under about half a second is inside the noise, which is why the two warm pip rows and the --no-cache-dir row are reported as the same number rather than as a ranking. The runner sits close to PyPI, so the download share of a cold install here is at the low end of what a slower network would show, and that biases against the download cache rather than for it. The three Poetry rows ran late in the same job and the two tools resolved different package counts, so nothing on this page compares pip with Poetry directly.
Frequently asked questions
Does the setup-python pip cache include the installed packages?
~/.cache/pip, and your job still runs the install afterwards. An open request on the action, "PIP cache should cache the installed packages as well", has been asking for the other behavior since 2022. If you want the install skipped rather than shortened, use Poetry or pipenv, whose cache options keep the virtualenv.Why does poetry install pick the wrong Python version after a cache restore?
pipx, set virtualenvs.in-project, and put the setup-python step after it, so the version the action selects is the version Poetry builds the virtualenv against.Should I cache the virtualenv directly with actions/cache?
How much cache does a Python project use against the 10 GB limit?
Related guides
References
- actions/setup-python: caching packages dependencies (verified 2026-09-20)
- pip documentation: caching, and the locally built wheel cache (verified 2026-09-20)
- actions/setup-python issue 330: cache the installed packages as well (verified 2026-09-20)
- GitHub Docs: caching dependencies to speed up workflows (verified 2026-09-20)
- Python documentation
- pip documentation
- GitHub Actions documentation