Baobaobaolin.com
date
entry
050
topic
tooling
rev
—

The go-build cache is back at 8.1G within three days of a clean: Go only deletes entries unused for five days, and every new go run executable is stored too

At 1 a.m. on October 18, this Mac's ~/Library/Caches/go-build was 8.1G. It had been cleared with go clean -cache only 68.5 hours earlier, and ten days before that as well. Go's build cache does clean itself, but the rule is to delete entries unused for five days, however big the total is; on a machine that builds every day, a few days' worth is enough to eat a large slice of the disk.

Go's build cache directory contains a README whose second line says: "Run "go clean -cache" if the directory is getting too large." go help cache says the go command periodically deletes data that hasn't been used recently, and that the cache correctly accounts for changes to source, compilers and options, so cleaning shouldn't be necessary in typical use. Together they sound like "it looks after itself; clean it if it really gets big."

But my du -sh records (terminal output from the time, not saved separately) say: 9.2G on October 5, cleared; 3.6G at 4:30 a.m. on October 15, cleared again; 100M at noon that day, and 8.1G again by 1 a.m. on October 18, cleared a third time. This note is about why the cache grows faster than it's cleaned. In order: the actual auto-clean rules, reproducing a trim in a separate directory, which builds make it grow fast, what each of the four go clean flags deletes, and what I plan to change.

Auto-clean: it looks at age, not size

go help cache only says "not used recently," and its "shouldn't be necessary" is about correctness, not disk usage. The numbers are in the source. Go 1.27.2's cmd/go/internal/cache/cache.go has three constants:

mtimeInterval = 1 * time.Hour
trimInterval  = 24 * time.Hour
trimLimit     = 5 * 24 * time.Hour

When a cache entry is read and its modification time is more than an hour old, go attempts to update it to now (the source calls this best-effort). Trimming isn't a background job: commands that run build work, like go build, go run and go test, close the cache and call Trim() as they finish (go list doesn't), which first checks trim.txt in the cache directory (the Unix time when the last trim started) and skips if it's been under 24 hours. When it does trim, it deletes entries whose modification time is older than five days plus one hour. A comment says five days comes from a month of usage statistics from Go developers (golang.org/issue/22990): essentially all reuse happened within five days of the previous use.

So the mechanism bounds age, and there's no size cap anywhere in this code. From the 4:30 a.m. clear on October 15 to the 8.1G at 1 a.m. on the 18th is 68.5 hours; nothing produced in that window reached the five-day threshold, so the auto-clean wouldn't delete any of it for age.

Watching it trim in a separate directory

I didn't want to touch the cache this machine actually uses, so I pointed GOCACHE at a scratch directory and ran the builds in the next section on a small program that imports net/http, up to 320,156 KiB. Then I used Python to set every file and subdirectory in it to a modification time six days ago, left trim.txt alone, and ran go build again: no change in size, because the last trim was under 24 hours ago. With trim.txt rewritten to a timestamp two days ago, another go build:

entries+dirs backdated 6d, trim.txt recent: 320156 KiB
trim.txt 2d ago, after go build: 103264 KiB  mach-o left: 0 0

Every executable stored by go run was deleted (they live in subdirectories ending in -d, so those directories' times have to be backdated too; backdate only the files and they survive). After the trim the total was 103,264 KiB with no Mach-O executables left. I didn't classify what remained; the change in total fits the rule that recently used or written entries stay.

What makes it grow fast

Same scratch cache, starting empty, measured with du -sk after each step (full output kept as exp.log in the experiment directory):

first go build (incl. the std packages used)   104,736 KiB
first go run                                   +3,408 KiB
edit a line + go run (five times)              +3,400 KiB each (6 executables in the cache, 20,568,300 bytes)
go run again without editing                   +0
edit + go build -o, three times                +144 KiB in total
first GOOS=linux GOARCH=amd64                  +85,736 KiB
first go build -race                           +109,132 KiB

The go run line only exists since Go 1.24. The Go 1.24 release notes say: "Executables created by go run and the new behavior of go tool are now cached in the Go build cache." They go on to say this comes "at the expense of making the cache larger." go build -o writes the executable where you tell it, and the cache gains only intermediate compile output; go run keeps the linked executable in the cache. Cache keys are hashes of the build inputs, so re-running without changes adds nothing; changing the program so it links an executable not yet in the cache adds a copy (revert to an earlier version still in the cache and it's reused).

This small program's executable is only 3.3 MiB. Real projects are much bigger: about ten minutes after the 1 a.m. clear on October 18, I measured again and the cache was already 719M (du -sh), including two Mach-O executables of 54.2 MB each, with timestamps about 17 seconds apart and sizes differing by 304 bytes. Several go run processes were running on the machine at the time, which fits an "edit, run again" pattern, but I didn't trace which process wrote which file.

Some hypothetical numbers for the order of magnitude: if a project's executable is 54 MB and you edit and re-run it 30 times a day, each time linking an executable not yet in the cache, that's about 1.6 GB a day; keep all of the last five days and it's about 8 GB. That's arithmetic to show the mechanism, not this machine's actual composition; I didn't break down what was in the 8.1G before clearing it. The first cross-compile to linux/amd64 for containers and the first -race build each add another set of compiled standard library and dependency packages that build used and the cache didn't have yet.

What each go clean flag deletes

go help clean lists four cache-related flags, and they delete different things:

-cache       the 00–ff subdirectories and log.txt in the build cache (not fuzz/)
-testcache   writes a timestamp that expires test results; build results stay
-modcache    the whole module download cache (GOMODCACHE, incl. unpacked source)
-fuzzcache   inputs saved by fuzzing (fuzz/ in the cache)

-cache is implemented in cmd/go/internal/clean/clean.go: it globs subdirectories matching [0-9a-f][0-9a-f], deletes them, then deletes log.txt, leaving the cache directory itself; a comment in cache.go also says fuzz/ is left to neither -cache nor the auto-trim. After go clean -cache on the scratch cache, the directory held only README and trim.txt, using 8 KiB. The cost is recompiling afterwards, and cached test results are gone too, so the next go test really runs; it doesn't touch modules or source, and it's the one of the four I'm most comfortable using. -testcache just writes a timestamp and frees no space right away.

-fuzzcache deserves a moment's thought. The docs describe these as inputs that "expand code coverage"; deleting them makes fuzzing less effective for a while, until inputs with the same coverage are found again. They aren't the corpus in testdata, which go clean never touches.

-modcache deletes downloaded modules. If later builds need them, they have to be downloaded again; with no network or a broken proxy at that point (and no vendor directory), the build fails. This machine's ~/go/pkg/mod is 4.7G, of which cache/download is 843M. Also, this directory doesn't come out cleanly with rm -rf. I downloaded one module (github.com/google/uuid@v1.6.0) into a scratch directory to try:

dr-xr-xr-x  … modcache2/github.com/google/uuid@v1.6.0
rm rc=1
files left:       31
rm: …/uuid@v1.6.0/go.mod: Permission denied
go clean -modcache rc=0

Unpacked module directories are read-only by default (unless you build with -modcacherw), and rm can't delete files inside a read-only directory. go clean -modcache makes the directories writable before deleting.

What I plan to change

Back to those three clears: each time I acted only when the disk was nearly full. Less than three days after the October 15 one, the cache was at 8.1G again. If I were setting this up again, I'd first decide how much this cache is allowed to take, then look at producing less, instead of waiting for a full disk.

Concretely, for projects I edit and run constantly, I'd switch to go build -o into a git-ignored directory in the project and run that, so executables stop piling up in the cache one copy at a time (intermediate compile output still goes into the cache). Occasional work like cross-compiling and -race can get its own GOCACHE, deleted wholesale afterwards, at the cost of recompiling whatever it would have shared with everyday builds. When space is needed right away, use go clean -cache; think before -modcache and -fuzzcache about whether you'll need them soon.

If you remember one thing

Go's build cache deletes only entries unused for five days and has no size cap; since 1.24, every distinct executable go run links is an entry too. On a machine that builds every day, its size depends on how much was recently added, how many older entries are still in use, and when trims actually run; it isn't just the last five days' output. To control the size, produce less or schedule your own cleaning; go clean -cache doesn't touch modules or source, and the module cache goes with go clean -modcache, not rm -rf.

Revision history

  1. First published