quickstart →

Remote caches

Remote caches for Bazel, Go, Gradle, sccache, Turborepo and other HTTP clients, served by the RunsOn agent from your S3 bucket with GitHub's branch scopes.

Build tools cache their work by key: Gradle task outputs, compiled Go packages, sccache’s compilations, Bazel’s actions, Turborepo’s tasks. Since v3.6.0, the RunsOn agent that already serves Magic Cache on every runner also serves those caches, from the cache bucket of your stack:

  • Nothing to deploy: no cache server, no credentials to manage. Entries stay in your AWS account.
  • Shared while jobs run: parallel jobs, matrix legs and later jobs reuse each other’s entries as soon as they are written. Nothing is restored or saved as one large archive.
  • GitHub’s branch scoping: a job writes to its own branch or pull request, and also reads its base or default branch’s entries. A pull request can use the default branch’s entries but never change them. Cache isolation covers them when it is enabled.

Enabling them#

List the tools in the remote_cache input of runs-on/action@v2 ↗, one per line, on a runner with Magic Cache (extras=s3-cache):

.github/workflows/ci.yml
- uses: runs-on/action@v2
with:
remote_cache: |
go
gradle
ToolLineWhat the action configures
Bazel (6.3+)bazel--remote_cache in Bazel’s system rc. See Bazel remote cache.
Go (1.24+)go, or go,tests=true to share test results tooGOCACHEPROG, a helper that keeps the build cache on local disk and in the agent. See the Go guide.
Gradle (7.6+)gradleAn init script that points Gradle’s HTTP build cache at the agent
mbx ↗mbxMBX_REMOTE_URL and MBX_REMOTE_NAMESPACE, see mbx
sccachesccacheRUSTC_WRAPPER and SCCACHE_WEBDAV_ENDPOINT. See the Rust guide.
Turborepo (2.2+)turboTURBO_API, TURBO_TOKEN and TURBO_TEAM, see Turborepo
Anything that stores entries over HTTPany of the aboveRUNS_ON_REMOTE_CACHE_URL, see other tools

Without Magic Cache, or on a RunsOn version before v3.6.0, the action warns and the tools build without the remote cache. A line with an unknown tool or option is skipped with a warning.

The sticky_cache input of the same action is different: it keeps a tool’s directories on the job’s sticky disk, restored when the same lineage runs again. remote_cache shares entries through S3 with every job and branch that can read them, while they build.

Gradle#

.github/workflows/ci.yml
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=16cpu-linux-x64/extras=s3-cache
steps:
- uses: runs-on/action@v2
with:
remote_cache: gradle
- uses: actions/checkout@v5
- uses: gradle/actions/setup-gradle@v6 # the distribution and dependencies
- run: ./gradlew build --build-cache

Builds use the remote cache when they have the build cache on, with org.gradle.caching=true in gradle.properties or --build-cache. The action writes its init script in Gradle’s user home; when GRADLE_USER_HOME is unset, it sets it to ~/.gradle, so that container jobs, whose HOME is not the account’s home, find the script. A build that configures an enabled remote cache of its own, such as Develocity’s, keeps it.

The build cache holds task outputs, not Gradle’s distribution, dependencies or compiled build scripts: keep setup-gradle for those. setup-gradle saves its cache from the default branch only, so on a pull request, every push rebuilds what earlier pushes changed. remote_cache: gradle keeps those outputs in the pull request’s scope, and shares them between parallel jobs.

On android/nowinandroid ↗ (assembleDemoDebug testDemoDebugUnitTest) on a 16 vCPU runner, the build took about 170 seconds without a cache:

Build stepNo cacheremote_cache: gradlesetup-gradleBoth
Cold172–182 s174–177 s––
Same commit again, on a new runner168–173 s74–76 s19 s21 s
  • Storing costs nothing measurable: a cold build stores every task output as it runs.
  • Without setup-gradle, most of the remaining time is spent downloading dependencies, which setup-gradle caches.

The init script names the job’s cache address, which changes from one job to the next, so a configuration cache ↗ restored from another job is not reused.

mbx#

.github/workflows/ci.yml
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/extras=s3-cache
steps:
- uses: runs-on/action@v2
with:
remote_cache: mbx
- uses: actions/checkout@v5
- uses: jdx/mr-boxington-action@v1.7.2
with:
backend: remote
- run: mbx build --workspace --locked

Set up mbx after runs-on/action, so its setup finds MBX_REMOTE_URL. Unless the job sets MBX_REMOTE_NAMESPACE, the action sets it to the repository’s ID. Each namespace gets its own path in the cache, so set MBX_REMOTE_NAMESPACE in the job’s env:, where the action sees it. By default, mbx publishes only from pushes to protected branches, so pull requests read what their base branch published. mbx does not send its own credentials over plain HTTP: a job that sets MBX_REMOTE_TOKEN, MBX_REMOTE_TOKEN_FILE or MBX_REMOTE_OIDC_AUDIENCE gets a warning, and keeps its own remote. So does a job that sets MBX_REMOTE_S3_* settings, which mbx refuses with an HTTP cache.

Turborepo#

.github/workflows/ci.yml
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/extras=s3-cache
steps:
- uses: runs-on/action@v2
with:
remote_cache: turbo
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- run: pnpm exec turbo run build test

The action points turbo at the agent’s remote cache ↗ with TURBO_API, and sets TURBO_TOKEN and TURBO_TEAM to runs-on: turbo needs both to turn its remote cache on, and the agent ignores them. turbo then prints Remote caching enabled, and cache hit, replaying logs for each task it restores.

  • Branch scoping: a job writes task outputs to its own branch or pull request, and reads its own first, then its base and default branch’s. The default branch warms the cache for pull requests, which can never change its entries.
  • Per platform: turbo’s task hash leaves out the runner’s OS and architecture, so each platform has its own entries.
  • Pull requests from forks read the cache without uploading: the action sets TURBO_CACHE=local:rw,remote:r for them, unless the job sets TURBO_CACHE.
  • Container jobs reach the agent too.
  • Signed artifacts (remoteCache.signature and TURBO_REMOTE_CACHE_SIGNATURE_KEY) work unchanged.

On vercel/ai ↗ (turbo build for its packages and three Next.js examples, 87 tasks) on an 8 vCPU runner:

BuildDurationTasks restored
No cache94–97 snone
Same commit again, on a new runner2.3–2.6 sall 87
On a new runner, after a one-line change in @ai-sdk/openai73 s48
On a new runner, after a one-line change in @ai-sdk/provider-utils, which most packages use74 s2

A change rebuilds its package and every task that depends on it. Both changes rebuild the three Next.js examples, the slowest tasks, so they take about as long.

pnpm install takes another 47 s in both cases: cache the pnpm store too, for example with pnpm/action-setup and actions/setup-node’s cache: pnpm.

A hit restores only the files a task lists in outputs in turbo.json, so list every file later steps need, and leave out files that change on every build, such as .next/cache/**. A job that already sets TURBO_API, TURBO_TOKEN, TURBO_TEAM or TURBO_TEAMID, for example for Vercel’s remote cache, keeps it: the action warns and leaves turbo alone. Remove them to use the agent’s cache.

Other tools#

Every tool of remote_cache exports RUNS_ON_REMOTE_CACHE_URL to later steps: $RUNS_ON_REMOTE_CACHE_URL/<namespace>/ is a plain HTTP cache that clients configure themselves, in a namespace of their own (1 to 64 lowercase letters, digits, -, _ or .):

  • GET and HEAD on <namespace>/<key> answer the entry, or 404. GET supports Range.
  • PUT stores the entry, replacing what was there. Conditional headers are ignored.
  • Entries are limited to 5 GB.

For example, ccache’s HTTP storage ↗:

.github/workflows/ci.yml
- uses: runs-on/action@v2
with:
remote_cache: go # or any other tool
- run: echo "CCACHE_REMOTE_STORAGE=$RUNS_ON_REMOTE_CACHE_URL/ccache/" >> "$GITHUB_ENV"
- run: |
sudo apt-get install -y ccache
cmake -B build -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
cmake --build build

How the job reaches its cache#

None of these tools sends GitHub credentials. The action registers the job’s runtime token with the agent once, and the agent answers with addresses derived from it, one per protocol, such as http://<runner IP>:6871/_runs-on/tools/<id>/kv:

  • The same token always gets the same addresses, nothing that lacks the token can compute them, and they reveal nothing of the token.
  • They work on that runner only, until the job’s token expires.
  • The action masks <id> in the job’s logs.

Storage#

  • Location: the cache bucket of your stack, next to Magic Cache entries, under the job’s branch or pull-request scope.
  • Expiry: entries expire with the bucket’s cache lifecycle rule (10 days by default), counted from when they were written. Tools rebuild what has expired.