Infrastructure worth keeping — and kept patched.

Is my build cache server healthy? What to alert on

Watch three things: the server answers, uploads succeed, and builds get hits. FosterStack Cache reports all three, but /healthz alone only says the process is running.

The short answer

What to alert on

Our advice, not a test result:

What not to alert on

Counters start again at 0 when the server restarts. Prometheus increase() handles the reset. Reading the raw value does not. The full list of metric names is in the monitoring section below.

Monitor a Gradle build cache with Prometheus and Grafana

The cache server exposes Prometheus metrics and a read-only status page. This section lists what is there, a scrape setting, a few queries, and where to get a ready-made Grafana dashboard.

Our advice, not a test result: the scrape setting and the queries are examples.

Metrics need no password

The /metrics and /healthz paths stay open even when Basic Auth is on, so a Prometheus scraper needs no credentials. The status page, /statusz, sits behind Basic Auth when it is enabled.

A scrape setting

scrape_configs:
  - job_name: fscache
    static_configs:
      - targets: ['cache.example.com:8080']

This is a plain example. Use your own address, and HTTPS if you have put the server behind a proxy.

The metrics

These are the names a running 0.2.1 server reports:

MetricTypeWhat it tells you
fscache_cache_hits_totalcounterRequests answered from the cache
fscache_cache_misses_totalcounterRequests for something not stored
fscache_store_bytesgaugeHow much is stored now. Reads 0 after a restart until the next upload
fscache_store_entriesgaugeHow many entries are stored. Reads 0 after a restart until the next upload
fscache_evicted_entries_totalcounterEntries removed to stay under the size cap
fscache_http_requests_totalcounterRequests, by method and status
fscache_http_request_duration_secondshistogramHow long requests take
fscache_bytes_read_totalcounterBytes sent to clients
fscache_bytes_written_totalcounterBytes received from clients

Queries worth having

# hit rate over the last 5 minutes
sum(rate(fscache_cache_hits_total[5m]))
  / clamp_min(sum(rate(fscache_cache_hits_total[5m])) + sum(rate(fscache_cache_misses_total[5m])), 0.0001)

# entries removed per second to stay under the cap
sum(rate(fscache_evicted_entries_total[5m]))

# 95th percentile request time
histogram_quantile(0.95, sum by (le, method) (rate(fscache_http_request_duration_seconds_bucket[5m])))

A ready-made Grafana dashboard

The repository includes a dashboard with eight panels: hit rate, cache size against the cap, entries, evictions, hits and misses, requests by status, throughput, and request latency. Download grafana-dashboard.json and import it in Grafana.

The status page

For a quick look without Prometheus, /statusz returns the numbers in one place, in a browser or as JSON. This is what a freshly started 0.2.1 server returned after one stored entry and two requests:

$ curl -s localhost:8080/statusz
{
  "version": "v0.2.1",
  "revision": "0bfc498ecfca3e38cb4a456acda44359a1620c5a",
  "fips140": false,
  "fips140_note": "off",
  "uptime_seconds": 2.118684875,
  "uptime": "2s",
  "store_bytes": 5,
  "max_bytes": 1073741824,
  "store_entries": 1,
  "cache_hits": 1,
  "cache_misses": 1,
  "hit_ratio": 0.5,
  "evicted_entries": 0,
  "auth_enabled": false
}

Related: Gradle remote build cache not working and Gradle build cache with Docker Compose.

How we checked

We ran real FosterStack Cache servers, one per case, and built a four-module Gradle project against each. After every step we asked /healthz and /statusz, read /metrics, and kept the build log.

What we did/healthz and /statuszWhat the counters and the build showed
Healthy server, cold build200 and 200Misses 13, PUT 201 13, bytes written 36,108, 13 entries, evictions 0
Same project again, new Gradle home200 and 200Hits 13, bytes read 36,108. Four compile tasks FROM-CACHE
One method changed in one module200 and 200One more miss, 12 more hits, 14 entries. Three compile tasks FROM-CACHE
Server stoppedcurl -sf exited 7, no HTTP status, same for /metricsNothing to read
Server started again on the same data200. /statusz still showed 38,639 bytes and 14 entriesAll counters 0 and uptime back to 0. /metrics showed 0 bytes and 0 entries until one upload, then 38,649 and 15
Start with a read-only data folderThe process exited with code 1. Probe: exit 7Log: write shutdown marker ... permission denied
Start where the data folder cannot be createdExit code 1. Probe: exit 7Log: check shutdown marker ... not a directory
Data folder made read-only while running200 and 200Existing entries: GET 200. New upload: 500, counted as PUT 500. Log: store error ... permission denied. Gradle: Could not store entry ... response status 500 then The remote build cache was disabled during the build due to errors. Build: BUILD SUCCESSFUL
Data folder deleted while running200 and 200. /statusz still showed the old sizeStored key: GET 404. New upload: 500, counted as PUT 500. Gradle: same two lines, BUILD SUCCESSFUL
Size cap 20,000 bytes, three builds200 and 200Evictions +6, +13, +13. Hits 0 each time. Stored size 19,694 to 19,727 of 20,000
One upload larger than the whole cap200413, counted as PUT 413
Logins on. No login, or a wrong one/healthz 200, /metrics 200, /statusz 401Reading an entry: 401. Nothing added to the request counters
Read-only login uploadsn/a403. Nothing added to the request counters
Gradle with a wrong passwordn/aCould not load entry ... response status 401: Unauthorized, remote cache disabled for the build, BUILD SUCCESSFUL

The two rules: rule 1 is the increase of PUT requests with a 5xx status. Rule 2 is evictions up while hits are flat. We worked out the difference between two saved /metrics scrapes with a small script. We did not run a Prometheus server or an alerting tool, so how you write these rules in your alerting tool is our suggestion.

Where the figures come from: a build asks the server for 13 things (four compile tasks and nine compiled build scripts), so one build adds 13 hits or misses. fscache_evicted_entries_total counts entries, not builds.

Setup: FosterStack Cache 0.2.1 and Gradle 9.8.0 on a Mac (arm64), Temurin 21.0.12.1, all on one machine over plain HTTP on the loopback address. Every build used a fresh copy of the project and a new empty Gradle home, with Gradle's local cache off. We broke the data folder with chmod and rm as the same user that ran the server.

The metric names and the status page output in the monitoring section come from a running 0.2.1 server.

Limits: we did not fill a real disk, we only used the size cap. We did not test Docker or Kubernetes probes, HTTPS, Maven, a slow disk, or a very large cache. The first runs were on a Mac; the second set, in the note below, was on Linux. A disk that fills up may behave differently from a cap or a read-only folder. For the exit with permission denied in a container, see the startup errors page. For what a build does while the server is down, see server down.

Second run: Linux, 0.2.2

The runs above were on a Mac with 0.2.1. We ran the table again on a GitHub-hosted Linux machine with FosterStack Cache 0.2.2, Gradle 9.8.0 and Java 21, on a four-module project of our own.

The run and its log are public.