skip to content

In a team that uses a shared remote build cache, why is it common to let only CI seed jobs push to the cache while developers pull read-only? How do you configure that split?

level: middleimportance: must knowfreq 55%

answer

  1. CI pushes, devs pull
  2. cache poisoning risk
  3. isPush gated, isEnabled separate
  4. buildCache {} in settings.gradle
  5. seed job on main branch

basics

~20 s

CI builds run in clean, trusted environments, so their outputs are reliable cache entries. Developers' machines vary and could poison the cache, so they only read. You set isPush = true on CI and false for developers in the remote(HttpBuildCache) block.

solid answer

~40 s

A remote build cache shares task outputs across machines. The risk is **cache poisoning**: if any machine pushes an output produced from a non-reproducible or misconfigured environment, every other build pulling that key gets a wrong artifact. CI runners are clean, pinned, and trusted, so their outputs are authoritative — they should **push** (seed the cache). Developer machines have local tweaks, dirty state, and varying toolchains, so they should **pull read-only** and never write. You configure this in `settings.gradle(.kts)`: ```kotlin buildCache { remote<HttpBuildCache> { url = uri("https://cache.example.com/cache/") isPush = isCi // true only on CI } } ``` The `isCi` flag is typically derived from an environment variable or a Gradle property. This keeps the cache trustworthy while still giving developers fast pulls.

code

kotlin · 9 lines
kotlin
// settings.gradle.kts
val isCi = System.getenv("CI") != null
buildCache {
  remote<HttpBuildCache> {
    url = uri("https://cache.example.com/cache/")
    isEnabled = true   // pull for everyone
    isPush = isCi      // push only on CI
  }
}

go deeper

for a junior

Know that CI writes and developers only read, and that this lives in the buildCache block.

for a middle

Explain the poisoning rationale and configure isPush gated on a CI flag in settings.gradle.kts.

for a senior

Distinguish isPush vs isEnabled, restrict push to a seed job, and reason about trust boundaries.

for a principal

Define org policy: which jobs seed, credential separation (write vs read tokens), and how the policy is enforced across many repos.

## The push/pull split A **remote build cache** is a key-value store of task outputs keyed by a hash of each task's inputs. When a build runs a `@CacheableTask` whose inputs hash to a key already in the cache, Gradle downloads the stored output (a **cache hit**) instead of executing the task. When the task runs and produces a new output, Gradle can **push** that output up to the remote so other builds benefit later. Two independent booleans govern remote cache participation per build: - `isEnabled` — whether this build talks to the remote cache at all. - `isPush` — whether this build is allowed to **write** new entries. Pulling (reading) happens whenever the remote is enabled; pushing is the gated privilege. ## Why restrict push to CI The cache is only as trustworthy as the machines that write to it. If a developer's machine has a patched dependency, a different locale, an uncommitted source change, or a non-reproducible task, and it pushes, the resulting entry is keyed by inputs that *look* identical to a clean build's. Everyone who later pulls that key gets a **poisoned** artifact — a wrong `.class` file, a stale resource — and the bug is maddening to trace because it appears to come from nowhere. CI runners, by contrast, are ephemeral, pinned to a known toolchain, and build only committed code. Their outputs are authoritative, so the convention is: **CI seeds (pushes), everyone pulls.** ## How to configure it In `settings.gradle.kts`, the `buildCache {}` block controls everything. You derive a flag for "am I CI" and gate `isPush` on it: ```kotlin val isCi = System.getenv("CI") != null buildCache { local { isEnabled = !isCi } // CI usually disables local cache remote<HttpBuildCache> { url = uri("https://cache.example.com/cache/") isEnabled = true // everyone reads isPush = isCi // only CI writes credentials { username = providers.gradleProperty("cacheUser").orNull password = providers.gradleProperty("cachePass").orNull } } } ``` A refinement: even within CI, restrict push to a dedicated **seed job** (e.g. a build on the main branch), not every PR build, so PR branches don't fill the cache with short-lived entries. ## Where this lives The `buildCache {}` block must be in `settings.gradle(.kts)`, not `build.gradle(.kts)`, because the cache is configured before projects are evaluated. You can also override per invocation with `--build-cache`/`--no-build-cache` or the `org.gradle.caching` property, but the push/pull policy itself is code in settings.

  • Does setting isPush = false also stop the build from reading the cache?
    No. Reading is governed by isEnabled; isPush only controls writing. With isPush = false and isEnabled = true the build pulls hits but never uploads new entries.
  • Why might you restrict push to only the main-branch CI job rather than all CI jobs?
    PR/feature-branch builds produce short-lived, churny entries that bloat the cache and add little reuse. Seeding from a stable main build maximizes hit rate for the entries everyone shares.

Like a Wikipedia where only verified editors can write articles but everyone can read them — readers trust the content because writes are controlled.

saying these in an interview costs you the question

  • Saying developers should push too 'so the cache fills faster' — that reintroduces poisoning risk.
  • Confusing isPush with isEnabled (thinking isPush=false disables reads).

context