Scaling Android CI: From 44 → 10 minutes — A Deep Dive into Build Time Optimization and Best…
Android monorepos get expensive and slow as they scale. We re-architected CI/CD caching, trimmed the Gradle graph, tuned the JVM…
Scaling Android CI: From 44 → 10 minutes — A Deep Dive into Build Time Optimization and Best Practices

Android monorepos get expensive and slow as they scale. We re-architected CI/CD caching, trimmed the Gradle graph, tuned the JVM, modernized the toolchain, and finally moved to Apple Silicon — plus specific configs and numbers you can replicate.
Context and Baseline
Our CI/CD pipeline started conventional: git clone → fetch npm and Android dependencies → execute Gradle build → notify; the build phase alone averaged ~35 minutes and repeatedly re-downloaded dependencies with no cache restore while configuration re-ran due to environment-driven invalidations. Each CI minute cost 2 credits.
After targeted optimizations, we cut total time to P90 = 11 minutes and P50 = 10 minutes, and reduced average credits per build from 55 to 43 (~20%).
At ~460 builds/month, we projected ~37% fewer credits and ~37% less wall-clock time, avoiding an imminent $22k/year tier-upgrade by staying on the current plan.
What Was Holding Us Back
- Archive cache irrelevance: as many modules changed, CICD cache decayed fast and reuse dropped.
- Configuration invalidations from system properties and env inputs caused re-configuration even when tasks didn’t change.
- Dependency resolution overhead: Gradle probed multiple repositories for each coordinate, adding latency at our scale.
- Variant bloat: CI compiled non-required variants, inflating the task DAG by thousands of tasks.
Machine constraints created OOMs and noisy variability that masked true performance.
Strategy Overview
We focused on four compounding pillars:
- Cache architecture with branch-aware keys, release/develop fallbacks, and a Gradle Remote Cache for task-level reuse.
- Build graph control to shrink the DAG: reduce inter-module coupling and enforce only required variants.
- Dependency resolution hardening via repository content filtering to avoid redundant lookups.
- Runtime tuning: right-size JVM heap/Metaspace for CI and disable work that helps locally but wastes time on ephemeral agents (e.g., configuration cache).
Optimization 1: Deterministic Caching with Branch-Scoped Keys
We made caching branch-aware and OS/arch-aware, and cached more than artifacts: Android SDK components, project dependencies, transformed artifacts, NPM cache, and NDK.
Restore priority was: current branch → previous release → develop.
# Gradle restore (priority order)
{{ .OS }}-{{ .Arch }}-gradle-cache-v3-{{ getenv "SWIGGY_CACHE_VARIANT" }}-{{ getenv "GIT_BRANCH" }}
{{ .OS }}-{{ .Arch }}-gradle-cache-v3-{{ getenv "SWIGGY_CACHE_VARIANT" }}-release
{{ .OS }}-{{ .Arch }}-gradle-cache-v3-{{ getenv "SWIGGY_CACHE_VARIANT" }}-develop
# Gradle save
{{ .OS }}-{{ .Arch }}-gradle-cache-v3-{{ getenv "SWIGGY_CACHE_VARIANT" }}-{{ getenv "GIT_BRANCH" }}
# npm restore/save
{{ .OS }}-{{ .Arch }}-npm-cache-{{ checksum "**/package-lock.json" }}
We normalized cache variants (release, releaseDebuggable) and gated cache saves to the default branch to reduce fragmentation and cost overhead:
# Decide cache variant and whether to save cache
cache_variant="prod"
if [ "$SWIGGY_APK_APP_VARIANT" = "release" ] || [ "$SWIGGY_APK_APP_VARIANT" = "benchmark" ]; then
cache_variant="release"
elif [ "$SWIGGY_APK_APP_VARIANT" = "releaseDebuggable" ]; then
cache_variant="releaseDebuggable"
fi
envman add --key SWIGGY_CACHE_VARIANT --value "$cache_variant"
save_cache=false
default_branch=develop
if [ "$GIT_BRANCH" == "$default_branch" ]; then
save_cache=true
fi
envman add --key PROJECT_DEFAULT_BRANCH --value "$default_branch"
envman add --key SWIGGY_SAVE_CACHE --value "$save_cache"
Early lesson: continuously saving branch-level cache added overhead that often outweighed benefits on our credit-per-minute plan; using develop/release caches (refreshed automatically on back-merges via GitHub Actions) gave better ROI.
Pricing plan tradeoff
Under the credit-per-minute plan (2 credits/min), the 1–1.5 minutes (sometimes ~3 minutes) to save cache increased direct cost, so we avoided branch-level cache saves. Once we moved to a per-build plan, time overhead wasn’t billed linearly, and we started saving branch cache post-build.
Post-Apple Silicon refresh
After moving to Apple Silicon, we set the base branch cache as the primary restore, then save a branch-level cache after the first run so subsequent, same-branch builds consistently hit single-digit minutes when changes are localized.
Optimization 2: Gradle Remote Cache (Module-Level Reuse)
CICD archive cache staled quickly as more modules changed, so we piloted a Gradle Remote Cache on a small EC2 (Linux, 8GB RAM, 256GB SSD) serving GET/PUT under /cache.
Results from the POC:
- Incremental builds: cache hit is now P50 = 44% and P90 = 56% (latest),
This raised incremental hit rates and stabilized day-to-day developer builds.
Optimization 3: Build Only What Matters (Variants + DAG)
We enforced building only required release variants in CI, cutting total Gradle tasks by ~26% (3399 → 2745).
We also reduced inter-module coupling across 100+ modules to widen parallelism and shorten the critical path — an ongoing effort with tangible impact.
Optimization 4: Repository Content Filtering
On clean builds, Gradle’s multi-repo probing added up. We introduced **repository content filtering** so Gradle fetched coordinates from the correct repo without trial-and-error, reducing resolver latency for our large graph.
Optimization 5: Right-Size the JVM for CI
We ran controlled trials and converged on Heap = 16GB and Metaspace = 12GB for 32GB runners, balancing GC frequency and pause time while avoiding OOMs.
Representatively, under the develop/release cache plan, end-to-end times routinely landed around ~20–22 minutes when cache hit was healthy.
We also disabled configuration cache in CI after build scans showed ~30 seconds spent saving a cache that’s not reused in ephemeral CI — useful locally, wasteful in pipelines.
Optimization 6: Machines That Don’t Fight You
We moved from Ubuntu 22.04 L to XL to eliminate OOMs; Bitrise’s SOC update (Google → AMD) improved times, and M1 Max trials reached ~15 minutes with availability caveats at the time.
We’ve since moved critical workflows to Apple Silicon (M4 Pro, 54GB), which tightened variance and removed memory headroom issues seen on earlier x86 profiles.
Toolchain Modernization
We bumped Kotlin, AGP 8.10.1, and the Gradle wrapper 8.11.1. Combined with the refreshed caching approach on Apple Silicon, this pushed subsequent, small/localized builds into single-digit minutes.
Results
Baseline vs improvements:
- Before optimizations on x86: P90 ≈ 44m, P50 ≈ 35m.
- After pipeline optimizations on x86: P90 ≈ 26m, P50 ≈ 23m; average credits per build 55 → 43 (~20% lower); ~37% monthly reductions in both credits and minutes; avoided a $22k/year tier-upgrade.
- After Apple Silicon (M4 Pro, 54GB) and cache/toolchain refresh: P90 = 11m, P50 = 10m.
Additional visible effects:
- “Time to start build” dropped from ~4–5 minutes to 30–90 seconds depending on cache source.
- In a comparable live window after rollout, we saved ~5800 credits (~45%).
What We’d Do Again (Best Practices You Can Lift)
- Use branch-aware, tiered cache keys and avoid saving branch caches on every run under per-minute billing; refresh authoritative develop/release caches automatically on back-merges.
- Adopt a Gradle Remote Cache for large modular repos to stabilize incremental builds and raise hit rates.
- Trim the DAG ruthlessly: enforce required variants only; stub or disable non-essential modules in CI to cut tasks.
- Constrain dependency lookups with repository content filtering to reduce resolver overhead.
- Tune the JVM to the runner and validate with CI trials; target predictable GC over edge-of-OOM settings.
Right-size machines and prefer Apple Silicon for Kotlin/AGP-heavy builds where available to compress build times.
Acknowledgements
I am Pratik Sahu from the Android Mobile team at Swiggy and I’d like to thank Sambuddha Dhar, Raj Gohil and Tushar Tayal. This milestone wouldn’t have been possible without their constant help and support
메타데이터
- post_id
- ff93aec82f05
- slug
- scaling-android-ci-from-44-10-minutes-a-deep-dive-into-build-time-optimization-and-best-ff93aec82f05
- url
- https://medium.com/swiggy-bytes/scaling-android-ci-from-44-10-minutes-a-deep-dive-into-build-time-optimization-and-best-ff93aec82f05
- canonical_url
- https://medium.com/swiggy-bytes/scaling-android-ci-from-44-10-minutes-a-deep-dive-into-build-time-optimization-and-best-ff93aec82f05
- author_url
- https://medium.com/@pratiksahu184
- status
- ok
- fetched_at
- 2026-07-11 07:22:59