Skip to main content
Gradle Build Optimizations

When Your Gradle Cache Eats 10GB but Builds Still Slow — What to Purge First

You check your disk space and there it's: Gradle's cache folder, sitting at 10.2 GB. You think, "Great, all those artifacts are ready to go. Should be fast." But your next build still crawls. You wait. And wait. Something's off. The truth: a huge cache doesn't mean a fast build. It often means the opposite — stale artifacts, duplicated dependencies, and configuration that's loaded from scratch every time. So what do you purge first? And what should you leave alone? Let's dig in. Where Does This Show Up in Real Work? The 10GB shock on a developer laptop You clone the repo on a Monday morning, run ./gradlew assembleDebug , grab coffee—and watch your SSD space drop by 10 gigabytes. That feels wrong. Especially when the build still takes four minutes. I have seen developers spend an afternoon digging through ~/.

You check your disk space and there it's: Gradle's cache folder, sitting at 10.2 GB. You think, "Great, all those artifacts are ready to go. Should be fast." But your next build still crawls. You wait. And wait. Something's off.

The truth: a huge cache doesn't mean a fast build. It often means the opposite — stale artifacts, duplicated dependencies, and configuration that's loaded from scratch every time. So what do you purge first? And what should you leave alone? Let's dig in.

Where Does This Show Up in Real Work?

The 10GB shock on a developer laptop

You clone the repo on a Monday morning, run ./gradlew assembleDebug, grab coffee—and watch your SSD space drop by 10 gigabytes. That feels wrong. Especially when the build still takes four minutes. I have seen developers spend an afternoon digging through ~/.gradle/caches, deleting whole module directories by hand, only to find the next build downloads everything again. The cache folder isn't one lump—it's a graveyard of artifacts, transient files, and old distribution wrappers. One team I worked with discovered six different Gradle wrapper versions, each holding onto its own 2GB cache. That's not optimization. That's hoarding.

CI agents with cache drives filling up

CI environments amplify the pain. A clean agent starts each build with zero cached artifacts, so every dependency downloads fresh. Teams react by setting a large ephemeral cache volume—say 20GB—and praying it survives more than two builds. The catch is that CI runners often share a single cache key space; one branch's plugin update invalidates everything. Now you have agents idling while they download Netty for the tenth time that day. Quick reality check—I have watched CI minutes double because nobody thought to isolate cache keys per Gradle version. The common fix? Just add more disk. That treats the symptom, not the rot.

The 'clean build' reflex—and why it makes things worse

When a build acts strange, the first instinct for many developers is ./gradlew clean. I understand the impulse. Sometimes it works. Mostly it throws away everything—including the one incremental compilation state that could have made the next run fast. The real problem is that clean doesn't discriminate: it torches build outputs, task caches, and file system watches in one sweep. One senior engineer I paired with had aliased gradle to gradle clean out of habit. His average build time was over seven minutes. Removing that alias dropped it to under three. That hurts—because nobody questioned the reflex.

'The cleanest build is the one you never run. But the second cleanest is the one where you know exactly what you're losing.'

— overheard in a CI channel after a 40-minute cache rebuild

On the team-wide cache front, the worst pattern I see is shared network cache directories mounted over NFS. Sounds efficient—one cache for twenty developers. The reality: file locking breaks, concurrent writes corrupt artifact metadata, and someone's buildSrc change triggers a global invalidation. Don't share a Gradle cache over NFS. You will spend more time debugging cache conflicts than you ever saved on download time. The safer bet is per-developer local caches with explicit CI storage policies—even if that means slightly more disk per machine. Disk is cheap. Developer time is not.

What Most People Get Wrong About the Gradle Cache

Build cache vs. dependency cache vs. module cache

I have stood beside developers staring at a 12-gigabyte .gradle/caches directory and heard the same phrase: ‘That’s a lot of cache — builds should be fast.’ It never is. The problem is almost always conflation — treating three separate caching systems as one monolithic blob. The dependency cache holds downloaded JARs and AARs; it’s mostly read-only after the first resolution, and a 5GB dependency cache is normal. The build cache stores task outputs — compiled classes, zipped archives — but only if tasks are actually reused across branches or machines. The module cache (often called ‘transformed artifact cache’) keeps unpacked and processed dependencies. Most teams don’t bother distinguishing between them, so when a build drags, they purge everything. That’s like throwing out your cookbooks because the kitchen sink is clogged.

The catch is that a large dependency cache does almost nothing for incremental build speed. Your JARs are already on disk. The real bottleneck is whether the build cache matches the current inputs. Huge cache directories often mask a nasty truth — most entries are from abandoned branches or stale CI runs. I have seen CI caches balloon to 20GB where 85% of entries had never been hit.

Why cache misses happen even with huge cache dirs

You can have ten gigabytes of cached output and still suffer a full rebuild. Why? The build cache is keyed on inputs — source code, annotation processors, dependency versions, even your JDK patch number. Change one transitive dependency hash, and the entire task chain misses. Many teams assume the cache functions like a general-purpose file store: more files equals more hits. Wrong order. The cache is a content-addressable map, not a history of everything you’ve ever compiled. A single compileOnly library bump can invalidate every downstream compilation task.

What usually breaks first is the assumption that caching is ‘set and forget.’ Quick reality check — most Android projects that use KSP or Room generate code with incremental compilers that produce different task signatures on every minor framework update. The cache size stays constant; the hit rate collapses.

‘The build cache is a contract, not a bucket. If inputs shift, the bucket empties — silently.’

— overheard during a Gradle Summit hallway conversation, 2023

The configuration cache — the silent performance killer

Most cache conversations focus on task outputs. That misses the actual first-second drain: configuration time. The configuration cache, introduced in Gradle 7, avoids re-running your entire build script on every invocation. When it works, it shaves fifteen seconds off cold builds. When it breaks — and it breaks often with dynamic plugins or custom tasks — it degrades to a full re-evaluation and still writes cache files. I have seen teams with a 500MB configuration cache that takes longer to load than to regenerate. The pitfall is that configuration cache failures are silent. Gradle falls back to a fresh evaluation and logs a warning most engineers never see.

Odd bit about development: the dull step fails first.

We fixed this by setting -Dgradle.configuration-cache.problems=fail in CI. That forced every warning into a visible error. Within a week, we removed three plugins that were injecting dynamic dependencies during configuration, and our cold build dropped from two minutes to forty-five seconds. The moral? Purge your configuration cache — yes — but first, verify you actually need it. If your build script has side effects or uses afterEvaluate extensively, the configuration cache is a net drag.

Patterns That Actually Speed Up Builds

Purging stale build cache entries first

Before you touch ~/.gradle/caches with a cleanup script, stop and look at what’s actually stale. I have seen teams delete everything every Monday morning as ‘routine maintenance’ — that burns 20 minutes per developer per week and defeats caching entirely. The real win is targeting caches/modules-2 and caches/transforms-3, not the entire tree. Run find ~/.gradle/caches -name '*.lock' -mtime +14 first; those locked files from old daemons are often the culprit. One team I worked with reclaimed 6GB simply by removing stale artifact transforms that Gradle had stopped referencing but never auto-evicted.

The tricky bit is that --refresh-dependencies masks the problem. That flag nukes your module cache and re-downloads everything — it’s a sledgehammer. Instead, use gradle build --build-cache --info and grep for FROM-CACHE: false entries. They tell you exactly which tasks missed cache. A single misconfigured annotation processor can invalidate 80% of your compile cache. I’ve watched a team halve their build time by fixing one room: the annotation processor that declared no inputs.

'We deleted the entire Gradle cache folder and gained nothing — the build was still slow because we never looked at what Gradle actually stored.'

— Senior dev, after a retrospective

Using --build-cache and --configuration-cache together

Most shops run one flag, not both. That’s like filling your car with gas but ignoring the spark plugs. --build-cache saves task outputs between machines; --configuration-cache serializes your entire configuration phase so Gradle doesn’t re-evaluate scripts every run. Alone, each shaves seconds. Together, they cut minutes — but only if you follow the rules. Disable build-cache for any task that writes to absolute paths or uses random ports during testing; those outputs are poisoned and will cache failures. The configuration cache is more brittle: it serializes Groovy closures by class, so any dynamic script that generates different configurations on the fly blows the cache. Quick reality check — add --configuration-cache-problems=warn in your gradle.properties and watch the log for Configuration cache entry discarded lines. That’s your hit list.

What usually breaks first is a plugin that mutates the project after evaluation. That causes a Configuration cache state, version 8.4, could not be reused error. When that happens, most teams disable the configuration cache entirely. Wrong move. Instead, pin the offending plugin to a known-safe version or wrap its logic in a afterEvaluate block. A 2023 survey of Android builds showed teams that kept both caches enabled saw 32% faster clean builds than those using either alone — but only after removing project.afterEvaluate hacks in custom plugins.

Setting up cache cleanup in CI pipelines

Your CI runner generates a new cache every commit. After two weeks your CI account is a landfill of partial outputs. The fix: a retention policy that deletes cache entries older than the last three successful builds on each branch. Jenkinsfiles and GitHub Actions both support gradle-build-action/cache-cleanup-after — but few teams actually set the key-fallback correctly. If your cache key includes ${{ github.ref }}, every branch starts cold. Use a composite key with the branch hash only and a restore-key that falls back to main. That yields ~70% hit rates on feature branches. The pitfall is storing .gradle/caches/journal — it grows unbounded and includes daemon lock files that corrupt across runners. Purge journal files in every CI job. I’ve seen a single misbehaving node swell that directory to 4GB in three hours because a failed build left 600 daemon lock attempts. Set a cron task: find ~/.gradle/caches/journal -type f -delete after every pipeline run. That alone keeps your CI cache healthy without full purges.

Anti-Patterns That Teams Often Try (and Revert)

Deleting the entire .gradle folder every week

I have seen teams script a cron job that nukes ~/.gradle every Monday morning. Feels righteous — a fresh start, no cruft. The problem is you just incinerated every cached artifact that was working fine. Next build? Every dependency redownloads, every task recompiles. Disk freed: maybe 4GB. Build time added: 12 to 18 minutes. That's not cleanup; that's a self-inflicted cold start. The catch is subtle — developers stop noticing the pain because they normalize the wait. But your CI bill will notice. A weekly purge guarantees your team rebuilds the entire dependency graph from scratch, including libraries that haven't changed since last year. The team that tried this at a mid-size Android shop reverted inside two sprints. They gained nothing except slower Monday standups.

Disabling the build cache entirely

Another classic: "The cache is broken, so let's turn it off." That solves nothing — it trades one problem for a worse one. Without a build cache, every local change forces a full recompilation of every module that transitively touches it. Quick reality check — a single line change in a shared utility class can trigger 40 minutes of rebuilds across five modules. Teams do this when cache invalidation bugs frustrate them, but disabling the cache is like pulling the fire alarm because the smoke detector chirps. You lose incremental compilation benefits, UP-TO-DATE checks vanish, and your developer experience regresses to 2015. One team I consulted for spent three months without a cache before admitting they'd lost nearly a full day per developer per week. They re-enabled it with a proper eviction policy — and kept their sanity.

What usually breaks first is the over-aggressive cache eviction policy. Some engineers set maxAge to 24 hours or trim the cache to 1GB. Wrong order. That hurts. The entire point of a cache is to keep objects that are expensive to recompute — so evicting aggressively ensures nothing survives long enough to accelerate the next build. You end up with a revolving door: artifacts arrive, get evicted, get rebuilt, get evicted again. Disk stays low, but build times spike because your cache hit ratio collapses toward zero.

'We cut our cache to 500MB and wondered why builds got slower. Turns out we were just rotating garbage in and out every hour.'

— Lead Android engineer, after reverting their disk-cap limit from 500MB to 8GB

Better approach: track your cache hit ratio before touching a single eviction knob. If hits are above 85%, leave the cache alone. If they're below 40%, the problem is not size — it's invalidation logic, stale inputs, or non-reproducible task outputs. Capping disk space before diagnosing the hit ratio is cargo-cult optimization. That's the anti-pattern in its purest form — treating symptoms while ignoring the actual pathology.

The Long-Term Cost of Ignoring Cache Drift

How cache entries become stale without cleanup

The build works on Tuesday. It works on Wednesday. By Friday, someone’s CI job takes twelve minutes instead of four, and nobody committed anything that should matter. That’s cache drift—the slow rot nobody logs. Each time a dependency changes its transitive hash, Gradle marks the old cache entry as potentially reusable, then never actually reuses it. The entry just sits there, eating disk, adding scanning overhead. I have seen projects where 40% of the cache directory contained artifacts that hadn’t been referenced in three months. The build didn’t fail—it just got slower, imperceptibly, until the team accepted ten-minute builds as normal.

Field note: android plans crack at handoff.

The hidden cost of incremental builds that aren’t really incremental

Most teams assume incremental means faster. Wrong interpretation—it means less wrong. When cache entries drift out of phase with the actual task inputs, Gradle falls back to a full relinking step but still calls it “incremental.” You get the worst of both worlds: the complexity of partial rebuilds without the speed. The catch is that no warning fires. No metric spikes. Your build duration just creeps up 2–3% per week until someone manually purges everything. We fixed this once by adding a weekly cache size threshold alert—anything over 8 GB triggered an automated cleanup of entries older than fourteen days. Build time dropped 30% the next Monday.

The long-term maintenance burden is worse than the speed loss. Every stale entry that survives a project upgrade becomes a landmine. Someone changes a plugin version, the cache doesn't invalidate correctly, and the next deployment pulls a half-warm build artifact from six weeks ago. That hurts. Not in a dramatic “the site is down” way—but in subtle runtime bugs that take three engineers two days to bisect. The cost of ignoring drift isn’t disk space. It’s trust. Once developers stop believing the build is reproducible, they start running full clean builds out of habit, and then your 10 GB cache problem becomes a 10 GB cache plus forty-minute builds problem.

The cache that never gets purged is the cache you can’t trust—and a cache you can’t trust is just slow storage with a bad attitude.

— overheard in a postmortem, after three teams had independently blamed “the build system” for a bug caused by stale outputs

Maintaining cache hygiene without manual intervention

Don’t appoint a cache czar. That’s cargo-cult maintenance. Instead, treat the cache like a database that needs vacuuming: scheduled, automated, and transparent. Two knobs matter: maxAgeInDays and maxSizeInMB on the remote cache, plus a local cleanup policy that runs before every CI pipeline. We set ours to evict anything older than seven days unless it’s been accessed in the last seventy-two hours. Simple rule, huge effect—the cache stays under 4 GB, and incremental builds actually feel incremental. The tricky bit is convincing your team that deleting stuff improves reliability. It does, but only if you own your cleanup policy and verify the output hash afterward. Skip that verification step, and you’re back to drift.

When You Should NOT Purge First

Network issues mistaken for cache problems

I once watched a team purge their entire ~/.gradle/caches folder three times in a single sprint. Builds still crawled. They blamed cache corruption, hash collisions, even the phase of the moon. The real culprit? A flaky VPN throttling artifact downloads to 200 KB/s. Every cache miss triggered a fresh fetch — and every fetch took ninety seconds. The cache was fine. The pipe was clogged. Quick reality check: if your build spends most of its time on ‘Downloading’ lines rather than ‘Executing’ lines, your problem lives outside Gradle. Purge your DNS cache first. Or switch to a wired connection. 10 GB of local cache means nothing when the network acts as a bottle.

The tricky bit is that a slow download looks like a cache problem. A developer sees the build hang, runs --refresh-dependencies, watches another long pause, and assumes the local store is corrupt. It isn't. They just flushed a working cache — now every teammate pays the same tax. We fixed this at Boomlyx by adding a five-line script that logs per-artifact download time across the team. Three engineers were on a throttled conference Wi-Fi. The cache was never the enemy.

Incremental build bugs that look like cache bloat

Here's a pattern I spot quarterly: a team deletes .gradle, rebuilds, celebrates a faster run — then three days later the build is slow again. They blame cache «drift». But what actually happened? They accidentally fixed an incremental build bug by destroying the evidence. An annotationProcessor configuration that didn't declare its outputs correctly. A task that used project.fileTree() without proper inputs. The cache held stale data, sure — but the root cause was a miswired task, not the cache itself. Purging just resets the clock.

Most teams skip this: run --info and look for «CACHED — not executing» versus «not cacheable». If you see tasks repeatedly marked «not cacheable» for reasons you don't understand, you have a declarative bug. The cache is doing its job — it's your build configuration that's lying to it. I've seen teams waste two weeks rotating cache keys when they should have added a single @InputDirectory annotation. Purge the misconfiguration, not the byte store.

«We wiped the cache every Monday ritualistically. Turned out our custom task was reading /tmp without declaring it. The Monday purge was just covering a seam.»

— build engineer at a fintech startup, after removing their weekly wipe script

When your build is slow because of tasks, not cache

Cache purging is a performance placebo when the build graph itself is pathological. Imagine you have a monolithic module that rebuilds 15,000 classes on any change. The cache hits. The incremental compiler works. But the task still takes forty seconds because :app:compileKotlin processes a single file by re-examining every source. No amount of cache scrubbing fixes a compiler invocation that scales with project size. The fix is module decomposition — or leveraging Gradle's --parallel with proper cross-module boundaries.

Another dead end: excessive configuration time. If your build spends twenty seconds in the configuration phase before any task runs, purging the cache won't touch that. The cache lives in the execution phase. Configuration time burns inside settings.gradle.kts, project plugins, and lazy evaluation gaps. A colleague once told me «I cleared cache and saved eight seconds». What they actually saved was a stale baseline — the next run without cache took the same twenty seconds to configure. The pain returned. The misdiagnosis didn't.

So when should you not purge first? When the build log shows long gaps between task execution, not inside it. When --profile reports more time in «Configuration» than «Task execution». When the team admits they haven't looked at the console output in weeks. Purge the assumption that the cache is always the problem. It's a powerful lever, but pulling it blindly trains your infrastructure to hide its real scars.

Open Questions & FAQ

Is it safe to delete individual cache files?

Yes — but only if you know exactly what you're grabbing. Gradle's cache directory (~/.gradle/caches/ on Linux or macOS) is a sprawling mess of JARs, transformed classes, build artifacts, and metadata. I have seen teams blindly rm -rf the entire folder and then wonder why the next CI run took forty minutes instead of eight. That hurts.

Reality check: name the development owner or stop.

The safe play: remove caches/modules-2/files-2.1/ if you're confident your remote repository is healthy. That's where downloaded dependencies live — and Gradle will re-download them on demand. The dangerous corner is caches/transforms-3/ or caches/transform-2/. Those hold preprocessed artifacts. Delete them and you lose incremental compilation gains, but the build still works. Trade-off: you save disk space, you lose speed for the next three runs.

What most people miss: the build-cache-* files inside caches/. Those are pure output hashes. Nuking them is painless — you just invalidate every cached task. Do that before you touch the dependency files. Wrong order costs a day.

How often should I purge the build cache?

The honest answer? Only when it hurts. The Gradle build cache — local or remote — is supposed to grow. I've worked on Android projects where the local cache hit 12GB and still returned cache hits in under two seconds. That's fine. The moment you see build times creeping up without code changes, you have cache drift, not cache bloat.

Purge when you've switched branches five times in a morning. Branch-hopping creates orphaned cache entries that never match the current task inputs. We fixed this by adding a weekly CI job that cleans the remote cache of entries older than seven days. Weekly. Not daily, not hourly. Overcleaning is the anti-pattern that teams try, then revert — because every purge triggers a full recompile for the first developer who hits the office on Monday.

One exception: after a Gradle version upgrade. I always purge local and remote caches then. The internal format changes subtly between versions — older entries won't match, but they'll still waste disk space. That's a one-time cost. Do it, move on.

Does the configuration cache ever need purging?

Yes, and this is where people waste hours. The configuration cache (.gradle/configuration-cache/) is separate from the build cache — it stores the resolved project model, not compiled outputs. It rarely needs manual purging. However, when it does go stale, the symptoms look exactly like a slow build: Gradle re-runs the entire configuration phase, scripts execute again, plugins reinitialize. Most teams skip checking this first.

Quick reality check—if your build spends 90 seconds in configuration but only 20 seconds on tasks, purge the configuration cache. Not the build cache. Not the dependency files. The seam blows out when you've refactored build scripts or changed plugin versions without a clean restart. I delete the .gradle/configuration-cache/ directory manually maybe twice a year. That's it. Overaggressive purging here buys you nothing but a cold start every morning.

'We spent three afternoons profiling incremental compilation before someone noticed the configuration cache was stale from a plugin update two weeks prior.'

— Lead engineer, mid-size Android team

So the takeaway? Don't shoot the messenger cache first. Diagnose which cache is actually misbehaving — dependency download, task output, or project configuration — then purge only that one. Disk space is cheap. Developer time is not.

What to Try Next (and What to Skip)

Run 'gradle --stop' before cache surgery

Most teams reach for rm -rf ~/.gradle/caches the second a build feels sluggish. I have done it myself—watched that 10GB folder vanish, then waited another 12 minutes for Gradle to rebuild the same JARs from scratch. Wrong order. The first thing to try is gradle --stop. This kills the long-running daemon that holds file locks and stale classloaders. I have seen a build drop from 4 minutes to 90 seconds simply because the daemon had accumulated 18 hours of heap garbage. Purge the daemon before you purge the cache—otherwise you're nuking files the daemon will just regenerate within the same session. That hurts.

Profile your build with --scan to find real bottlenecks

gradle --scan generates a Build Scan™—a web page that exposes exactly where time goes. Most teams skip this: they guess the cache is bloated, but the real culprit is configuration time (applying plugins lazily) or resolution time (too many dynamic versions). I once debugged a project where 40% of the build was spent resolving latest.integration dependencies—something cache purging never touches. The scan shows you task timings, cache hit ratios, and warning banners about non-incremental tasks. Run it once. You may find your 10GB cache is not the problem—it's the 200 tasks that skip their inputs.

The tricky bit is that Build Scans require internet access and a Gradle Enterprise account for non-public projects. But even the free tier on scans.gradle.com gives you enough data to decide: do you purge the cache, or do you refactor your build.gradle? That call is impossible without a scan.

'We cleared the cache every Monday for three months. Then a developer ran —scan and discovered one annotation processor was double-compiling 800 files.'

— Lead architect, Android CI team, after switching to targeted task tweaks instead of full cache wipes

Set up a weekly cache cleanup script

Automate the purge you were doing manually—but make it surgical. A cron job that runs every Sunday at 2 AM: find ~/.gradle/caches -name '*.jar' -atime +14 -delete. This removes JARs untouched in two weeks, which catches stale snapshots without blowing away your hot cache. We did this on a monorepo that downloaded 300+ artifacts per build; the cache stabilized around 4GB instead of creeping to 12GB. The catch is that atime depends on your filesystem mount options—ext4 with noatime breaks that approach. Use ls -lu to verify access times before trusting the script in production.

What to skip entirely: manual cache deletion after every major merge, third-party "Gradle cleaner" plugins that claim to optimize residency, and setting org.gradle.caching.debug=true on CI unless you enjoy log dumps larger than the cache folder itself. And don't throw more memory at the daemon until you have confirmed GC pauses are the bottleneck—I have seen teams allocate 6GB of heap to a 20-task build and wonder why nothing improved. Profile first, script second, purge last.

Share this article:

Comments (0)

No comments yet. Be the first to comment!