Why Is the uv Cache So Large on My Mac?
Coding agents fill the uv cache when they launch MCP servers with uvx; prune it safely and see how much disk you really get back.

On this page
TL;DR
Quit every coding agent and editor that launches MCP servers, then run uv cache prune. A running session holds the cache lock, so prune prints “Cache is currently in-use, waiting for other uv processes to finish” and, after uv’s lock timeout, gives up. Do not use --force to skip it. Expect much less back than prune reports: on this Mac it reported 111.1 GiB (about 119 GB) removed and the disk gained 27.5 GB, most likely because cloned files share blocks.
$ uv cache size -H
$ df -h /
$ diskutil info / | grep "Container Free Space"
$ lsof "$(uv cache dir)/.lock"
$ uv cache pruneMy Mac was down to 38.4 GB free on a 1 TB disk. The largest directory on it was a 132.7 GiB uv cache that I had never thought about. The coding agents I use launch MCP servers with uvx, and that was enough to fill it.
uvx is uv’s command for running a Python tool from a temporary environment, without installing it. If you have never run uv by hand, an agent or editor ran it for you. This page is part of a series on cleaning up a developer Mac’s disk. Part one of the series, finding what’s filling the disk, is how to know the cache is the cause; you don’t need it to follow the steps here.
Tested with
- uv
- 0.11.12
- macOS
- 27.0.1, Apple silicon, APFS
- Date
- October 2026
Check the size and location of the cache
The environments uvx builds go in uv’s cache. Ask uv for the size and the directory:
$ uv cache size -HOutput
warning: `uv cache size` is experimental and may change without warning. Pass `--preview-features cache-size` to disable this warning.
132.7GiB$ uv cache dirOutput
/Users/<user>/.cache/uvuv labels uv cache size as experimental, which is the reason for the warning. The number is still the one to start from. The --cache-dir option, the UV_CACHE_DIR and XDG_CACHE_HOME variables and the tool.uv.cache-dir setting move the directory, according to uv’s cache documentation (opens in a new tab). Every command below reads the directory from uv cache dir, so it follows a moved cache.
Find what fills the cache
Environments that uvx builds sit in the cache’s archive-v0 directory. Count its entries, then count the ones that are Python environments, which have a pyvenv.cfg at their root:
$ ls "$(uv cache dir)"/archive-v0 | wc -l
$ find "$(uv cache dir)"/archive-v0 -maxdepth 2 -name pyvenv.cfg | wc -lOn this Mac, archive-v0 held 4,005 entries, and 2,562 of them were environments. To see which MCP servers the environments belong to, count the executables with mcp in their names in the environments’ bin directories (a server named otherwise needs its own pattern):
$ ls -d "$(uv cache dir)"/archive-v0/*/bin/*mcp* | xargs -n1 basename | sort | uniq -c | sort -rn | headNearly all of the environments were copies of three MCP servers, with 1,150, 1,143 and 191 copies, plus 50 of a fourth. One server’s environments held only 158 distinct sets of installed packages, and environments built on the same day had the same installed packages.
Why each launch can add an environment
An MCP server configured as uvx <server>@latest asks uv to resolve the latest version on every launch. With uvx -v, the log shows uv fetching the latest version and revalidating the index. Launching one server did not explain more than a thousand copies, so I measured when a launch adds an environment:
| Launches | Resolution | Environments added |
|---|---|---|
| One | New (not cached yet) | 1 |
| Four at the same moment | New | 4 |
| Four at the same moment | Already cached | 0 |
Four, @latest and plain | Already cached | 0 |
When several sessions start together and the resolution isn’t cached yet, each launch builds its own environment. Once the resolution is cached, launches add nothing. That implies pinning a version, instead of @latest, would stop most of the copies. I didn’t test that. I also didn’t check whether a later release of uv reuses an environment that another process built moments earlier. All of this is uv 0.11.12.
Choose between uv cache prune and uv cache clean
uv cache prune removes the cache entries nothing uses, and all centralized project environments, which uv recreates as needed. uv cache clean removes every entry and empties the cache; uv cache clean <package> removes one package’s entries. Both are described in uv’s cache documentation (opens in a new tab). This page uses prune, which here took the cache from 132.7 GiB to 3.9 GiB and left 540 entries in place. I didn’t run clean.
Quit every session that holds the cache lock
Each running uvx process holds the cache lock, the .lock file in the cache directory, for as long as its server runs. Cache-modifying commands wait for the lock, so a prune started now waits too. List the holders:
$ lsof "$(uv cache dir)/.lock"Output
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
uv <pid> <user> 9u REG <dev> 0 <inode> /Users/<user>/.cache/uv/.lock
uv <pid> <user> 9u REG <dev> 0 <inode> /Users/<user>/.cache/uv/.locklsof prints nothing when the lock is free; empty output from a path that is not your cache directory is not a clear lock. Each row is one uvx process. To find the session that started it, look up the parent of each <pid>:
$ ps -o ppid= -p <pid>
$ ps -o comm= -p <ppid>On this Mac, one holder was launched by a Claude Code session and another by the VS Code extension host. Quitting a session or an editor ends its uvx children, which release the lock:
- Run
lsof "$(uv cache dir)/.lock". - Look up the parent of each
<pid>. - Quit that session or editor.
- Run
lsofagain until it prints nothing.
Record free space before pruning
The comparison needs readings from before the prune, so take them now and save the output:
$ df -h /
$ diskutil info / | grep "Container Free Space"
$ uv cache size -HRun the prune
This command deletes cached environments. With nothing running, it removed every environment in the cache, not only the ones that no process referenced. The next launch of each MCP server builds a fresh environment. Run it from a plain Terminal window, not from inside one of the sessions you quit:
$ uv cache pruneOutput
Pruning cache at: .cache/uv
Removed 7278379 files (111.1GiB)It took 409 seconds. Do not interrupt it.
Check the space you got back
The free space went up by much less than 111.1 GiB. Run the same three commands again and compare them with the readings you saved:
| Reading | Before | After |
|---|---|---|
df -h /, available | 36Gi | 61Gi |
| Container Free Space | 38.4 GB (38,372,454,400 bytes) | 65.8 GB (65,847,853,056 bytes) |
uv cache size -H | 132.7GiB | 3.9GiB |
The container gained 27.5 GB. uv reported 111.1 GiB removed, which is about 119 GB. uv prints binary units (GiB, 2^30 bytes) and diskutil prints decimal ones (GB), so do not compare the two figures as equals. Even in the same unit, the disk got back about a quarter of what uv reported.
The explanation that fits is the link mode; I didn’t test it directly. uv’s default on macOS is clone, per uv’s settings reference (opens in a new tab), which makes APFS copy-on-write clones. Environments share blocks with each other and with the cached wheels. uv 0.11.12 counts every cloned file at its full size, and deleting a clone frees nothing when another file still uses its blocks. uv’s cache documentation (opens in a new tab) describes a cache-physical-space preview feature that accounts for copy-on-write clones; I didn’t test it.
du -sh agreed with uv cache size at 3.9G after the prune. That is a drop of 128.8 GiB from 132.7GiB, while prune printed 111.1GiB removed; I did not find why the two differ.
A local snapshot could also hold space after a delete. On this Mac, tmutil listlocalsnapshots / listed none, and diskutil apfs listSnapshots showed none on the data volume, so no snapshot was holding any.
Confirm your MCP servers still start
The prune left archive-v0 with 540 entries and no environments, and emptied environments-v2. Start each agent or editor that uses MCP servers and open a session. Each server builds a fresh environment on its next launch; on this Mac, one appeared at the next session start. Check:
$ find "$(uv cache dir)"/archive-v0 -maxdepth 2 -name pyvenv.cfg | wc -lA count above zero means at least one server built an environment. Check each agent’s MCP server list or status view for errors.
Do this with a coding agent
Paste this prompt into your own local coding agent. It measures and reports, and it proposes before anything is removed:
Investigate my uv cache on this Mac. Measure and report only; change nothing yet.
1. Report the cache directory (uv cache dir) and its size (uv cache size -H).
2. Report how many entries in archive-v0 are Python environments (a pyvenv.cfg
at the root), and which tools they belong to (the executables in their bin
directories).
3. Report free space two ways: df -h / and diskutil info / (Container Free
Space).
4. List every process holding the cache lock (lsof on the .lock file in the
cache directory), and for each one its parent process, so I can tell which
agent or editor session to quit.
5. Separate what is safe from what is in use: running uv processes, open agent
or editor sessions, and any project whose own virtual environment lives
inside the cache. If this session is itself one of the lock holders, say so
and stop; I will run the prune from a plain Terminal.
Then propose each removal as a separate item, with the command and the current
cache size as an upper bound. uv has no dry run, so do not state an expected
figure, and note that APFS clones make the real gain smaller. Remove nothing until I approve that item. Remove only
through uv's own command (uv cache prune). Never delete files under the cache
directory by hand and never use --force. Afterwards, report free space again
with the same two readings and say how far the figure uv printed is from the
space actually gained.Common failures
Cache is currently in-use, waiting for other uv processes to finish
Error
Cache is currently in-use, waiting for other uv processes to finish (use `--force` to override)Another uv process holds the cache lock, and uv cache prune and uv cache clean wait for it. Run lsof "$(uv cache dir)/.lock", quit the session that owns each holder as described earlier, and run the command again. Nothing was changed while it waited: I stopped a prune that had waited eight seconds, and the cache was unchanged.
These uv cache commands give up after a timeout; uv’s cache documentation (opens in a new tab) gives its value and the UV_LOCK_TIMEOUT environment variable that changes it. Waiting longer does not help while a session still runs its server.
Do not use --force. The documentation reserves it for cases where no other uv process is reading or writing the cache, and a running uvx server is exactly such a process.