Infrastructure worth keeping — and kept patched.

Build cache size limit, eviction and a full disk

A self-hosted build cache cannot grow forever. FosterStack Cache has a size cap, and when it is reached it removes the entries used least recently. This page shows what that looks like in real 0.2.1 runs, and what happens when the disk itself fills up.

Setting the cap

Set FSCACHE_MAX_BYTES, in bytes. The default, 0, means no limit. Real servers use gigabytes; see server sizing for values you can copy.

How the size cap works

What happens when the build cache disk fills up?

In our runs, a full disk did not break builds. The cache server refused new uploads and kept serving what it already held. Gradle still ended with BUILD SUCCESSFUL.

What to do

Our advice, not a test result:

Watching it on a real cache

The same numbers are in /statusz as store_bytes, max_bytes, store_entries and evicted_entries, and in the Prometheus metrics (fscache_evicted_entries_total and fscache_store_bytes), except that the two store gauges read 0 after a restart until the next upload. A rising eviction count together with a hit rate that never climbs means the cap is too small for your build. See what to alert on.

How we checked

A deliberately tiny cap

FosterStack Cache 0.2.1. We used 3500 bytes, so that a few small entries fill it:

FSCACHE_MAX_BYTES=3500 ./fscache

We stored three 1,000-byte entries, read one of them, then stored two more. After each step we looked at /statusz, the status page, and tried to read every key back.

StepStored afterwardsEvicted so farWhich keys could be read
Store k1, k2, k33,000 bytes, 3 entries0all three
Read k1unchanged0k1 is now the most recently used
Store k4 (would go over 3,500)3,000 bytes, 3 entries1k1, k3, k4. k2 is gone.
Store k53,000 bytes, 3 entries2k3, k4, k5. k1 is gone.

A real full disk

FosterStack Cache 0.2.1 on a Mac (arm64), no password, and no size cap unless a row says so. We made a real 8 MiB disk image with APFS, mounted it, and put the data folder on it. Gradle 9.8.0 and Java 21 built a four-module project with a new Gradle home each time, no reused daemon and the local build cache off. Each row is one run, and we did not repeat them.

What we didWhat happened
Cold build, disk had roomStored 13 entries, 36,091 bytes
Sent 256 KiB uploads until one failed21 accepted, the 22nd got 500 with the body internal error
Then 4 KiB uploads34 accepted, the 35th got 500. A 1 KiB upload then also got 500
Looked at the full serverProcess running. /healthz answered ok. /statusz showed 68 entries and 5,680,379 bytes. Four PUT requests with status 500 were counted
Read stored entries200 for both keys we tried. A key never stored gave 404
Same build again, disk fullBUILD SUCCESSFUL, 4 tasks from the cache
Changed build that needed to store, disk fullBUILD SUCCESSFUL. One Could not store entry ... response status 500: Internal Server Error, then The remote build cache was disabled during the build due to errors.
Deleted a 1 MiB file outside the data folderA 1 KiB upload and three 256 KiB uploads were accepted. The next two got 500. No restart
Changed build again, with roomBUILD SUCCESSFUL and no warning
Stopped the server and started it on a full volume with its old dataIt started. /healthz answered ok, /statusz showed 127 entries, a stored entry read 200, a new upload got 500
Started it with a new, empty data folder on a full volume (a 4 MiB size cap was set)It exited and never answered. The log held fscache: fatal and open metadata store
Size cap 4 MiB, 40 uploads of 256 KiB (10 MiB in all)All 40 accepted. 24 entries removed. Stored size stayed at 4,194,304 bytes
Size cap 20 MiB, the same 40 uploads25 accepted, 15 got 500. Nothing was removed

We found no leftover temporary files in the data folder after the first failed uploads. On this small APFS volume, uploads started to fail while df still showed about 1 MiB free, so do not plan around the last megabyte.

Not tested: Docker or Kubernetes volumes, file systems other than APFS (the second run below adds a Linux RAM disk), Maven, a very large single upload, a full inode table, a disk quota, releases other than 0.2.1 and 0.2.2, and a disk that fills while many builds upload at once.

Try the tiny-cap run yourself

FSCACHE_ADDR=127.0.0.1:8080 FSCACHE_DATA_DIR=./data FSCACHE_MAX_BYTES=3500 ./fscache &
until curl -s localhost:8080/healthz >/dev/null; do sleep 0.2; done   # wait until it is up

put() { head -c 1000 /dev/zero | curl -s -o /dev/null -w "PUT $1 %{http_code}\n" -X PUT --data-binary @- localhost:8080/$1; }
get() { curl -s -o /dev/null -w "$1=%{http_code} " localhost:8080/$1; }
put k1; put k2; put k3
get k1; echo
put k4
for k in k1 k2 k3 k4; do get $k; done; echo
curl -s localhost:8080/statusz | grep -E 'store_bytes|evicted'

# the output of the middle line in my run:
# k1=200 k2=404 k3=200 k4=200

Related: how to clear a build cache and Gradle and Maven setup.

Second run: Linux, 0.2.2

The runs above were on a Mac with 0.2.1. We ran the tiny-cap table and the size-cap runs again on a GitHub-hosted Linux machine with FosterStack Cache 0.2.2. For the full-disk runs we used an 8 MiB RAM disk there (tmpfs, a small disk that lives in memory, so it can be filled safely).

The run and its log are public.

Server sizing FosterStack home