Skip to content

Gradle and Bazel Remote Build Cache

View as Markdown

A CloudRepo Maven repository can be the remote build cache for Gradle (HttpBuildCache) and for Bazel (--remote_cache). Both clients store a cache entry with an HTTP PUT and read it back with a GET, and the repository returns each entry byte for byte. There is no separate repository type for this: you create one repository for the cache, change one setting on it, and point your builds at it.

The settings below were measured with Gradle 9.7.1 and Bazel 8.4.2.

  1. Create a Maven repository for the cache only, such as gradle-cache or bazel-cache. See Create Your First Repository. Keep it apart from the repositories that hold your releases.
  2. Turn Overwrite Protection off on that repository, from the Overwrite Protection card in its settings. See Overwrite Protection.

A new repository has Overwrite Protection on, so it refuses a second upload of a key it already holds with 409 Conflict and the message Version already exists. Overwrites are disabled for this repository. A build cache sends such uploads routinely: a build that runs its tasks again (Gradle’s --rerun-tasks), two jobs that miss the same key at the same time, or two Bazel actions whose outputs are the same bytes.

  • Gradle logs Could not store entry, then The remote build cache was disabled during the build due to errors., and stops using the remote cache for the rest of that build.
  • Bazel logs a warning and does not store that action’s result, so the action misses in every later build.

Both builds still succeed, so the log is the only place this shows. With Overwrite Protection off, the repository accepts the repeated upload and both clients keep their cache.

Authenticate with repository tokens, not your password. See Repository Tokens: Create One and Authenticate for how to create one, and which account to create it as.

  • For CI, a Read + write token that reaches only the cache repository. CI fills the cache.
  • For developer machines, a Read only token for the same repository. It downloads cache entries; an upload with it is refused with 403 Forbidden. Turn uploads off on developer machines too (shown for each client below), so their builds only read.

The username is the email address of the account that created the token, and the password is the token. The examples read both from two environment variables, CLOUDREPO_USERNAME and CLOUDREPO_TOKEN. Never commit a real token to version control.

Add the remote cache to settings.gradle or settings.gradle.kts:

settings.gradle
buildCache {
remote(HttpBuildCache) {
url = 'https://your-org.mycloudrepo.io/repositories/your-repo/'
push = System.getenv('CI') != null
credentials {
username = System.getenv('CLOUDREPO_USERNAME')
password = System.getenv('CLOUDREPO_TOKEN')
}
}
}
settings.gradle.kts
buildCache {
remote<HttpBuildCache> {
url = uri("https://your-org.mycloudrepo.io/repositories/your-repo/")
isPush = System.getenv("CI") != null
credentials {
username = System.getenv("CLOUDREPO_USERNAME")
password = System.getenv("CLOUDREPO_TOKEN")
}
}
}

Then turn the build cache on, with org.gradle.caching=true in the project’s gradle.properties or --build-cache on the command line.

push is on only where the CI environment variable is set. GitHub Actions, GitLab CI/CD and CircleCI set CI=true in their jobs; on another CI service, set it in the job. On a developer machine, where CI is not set, Gradle reads from the cache and uploads nothing.

Add two lines to the workspace’s .bazelrc:

.bazelrc
build --remote_cache=https://your-org.mycloudrepo.io/repositories/your-repo
build --credential_helper=your-org.mycloudrepo.io=%workspace%/tools/cloudrepo-credential-helper.sh

Bazel asks the credential helper for the header to send. Save this one as tools/cloudrepo-credential-helper.sh in the workspace and make it executable:

tools/cloudrepo-credential-helper.sh
#!/bin/sh
# Bazel credential helper for CloudRepo. Bazel writes a request on stdin; this answers with a
# Basic Authorization header built from two environment variables.
cat > /dev/null
printf '{"headers":{"Authorization":["Basic %s"]}}\n' \
"$(printf '%s:%s' "$CLOUDREPO_USERNAME" "$CLOUDREPO_TOKEN" | base64 | tr -d '\n')"
Terminal
chmod +x tools/cloudrepo-credential-helper.sh

On developer machines, add this line to your own ~/.bazelrc so Bazel reads from the cache and uploads nothing:

~/.bazelrc
build --remote_upload_local_results=false

Cache entries are ordinary files in the repository. Gradle stores each entry at the repository root, named by a 32 character key. Bazel stores its entries under ac/ and cas/, named by a SHA-256 digest. A key nobody stored answers 404 Not Found, and both clients read that as a cache miss.

To empty the cache, delete its files in the portal. See Deleting Files and Folders.

Each cache upload is a file upload and each cache hit a file download. Each one fires the repository’s File Upload or File Download webhooks, if it has any. See Webhooks. A build can store and read many entries, so leave webhooks off the cache repository unless you want one event for each.

What you see Why What to do
Gradle: Could not store entry, then The remote build cache was disabled during the build due to errors. Overwrite Protection is on: the repository refused a repeated upload with 409. Turn Overwrite Protection off on the cache repository.
Bazel: a warning about a refused upload, and the same action misses in later builds The same 409. The same.
403 Forbidden on an upload The token is Read only, or it does not reach this repository. On developer machines, turn uploads off as shown above. In CI, use a Read + write token that includes the cache repository.
401 Unauthorized No credential, or one CloudRepo refused: a mistyped, expired or revoked token, or the wrong username. Check the token and the username as in When a publish is refused.