Staged rollouts reduce risk by releasing to a small percentage of users first, monitoring for crashes, and expanding only when metrics are healthy. Hotfix flows let you ship critical fixes fast without going through the normal release cycle.
Staged Rollout on Google Play
1% → monitor 2h (crash rate, ANR rate, 1-star reviews)
10% → monitor 12h
25% → monitor 24h
50% → monitor 24h
100% → full release
Automating with Fastlane
# Fastfile
lane :deploy_staged do
upload_to_play_store(
track: "production",
rollout: "0.01", # 1%
aab: "app/build/outputs/bundle/productionRelease/app-production-release.aab",
skip_upload_apk: true,
release_status: "inProgress"
)
end
lane :expand_rollout do |options|
percentage = options[:percentage] || "0.1"
upload_to_play_store(
track: "production",
rollout: percentage,
release_status: "inProgress",
version_code: options[:version_code]
)
end
Pause and Halt
If crash rate spikes during staged rollout:
- Pause rollout in Play Console (stops expansion; existing installs keep the version)
- Halt rollout (marks version as halted; users still on old version won't update)
- Fix the bug
- Submit a new version; restart rollout
Release Branch Strategy
main (production)
├── release/3.2.0 → tags v3.2.0, v3.2.1 (hotfix)
├── release/3.3.0 → next release cycle
└── feature/* → day-to-day development
Hotfix Flow
A hotfix is a critical fix that bypasses the normal feature branch → develop → release flow:
# 1. Branch from the production tag
git checkout v3.2.0
git checkout -b hotfix/3.2.1-crash-on-checkout
# 2. Apply the minimal fix
git commit -m "fix: NPE in CheckoutViewModel when cart is empty"
# 3. Tag and push
git tag v3.2.1
git push origin hotfix/3.2.1-crash-on-checkout --tags
# 4. Merge back to both release and main
git checkout release/3.2.0
git merge hotfix/3.2.1-crash-on-checkout
git checkout main
git merge hotfix/3.2.1-crash-on-checkout
Hotfix CI Pipeline
# .github/workflows/hotfix.yml
name: Hotfix Release
on:
push:
tags: ['v*.*.*']
jobs:
release-hotfix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get tag version
run: |
echo "VERSION_NAME=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Set version code
run: echo "VERSION_CODE=$(date +%Y%m%d)02" >> $GITHUB_ENV
- name: Build release bundle
env:
# ... signing env vars
run: ./gradlew bundleProductionRelease
- name: Upload to Play internal track
run: fastlane deploy_hotfix
# Then manually promote to staged rollout once validated
Remote Config for Feature Flags (Kill Switch)
A kill switch lets you disable a broken feature without a code push:
class FeatureFlagManager(private val remoteConfig: FirebaseRemoteConfig) {
init {
// Set sensible defaults (features ON by default)
remoteConfig.setDefaultsAsync(
mapOf(
"checkout_v2_enabled" to true,
"new_search_enabled" to true
)
)
}
val isCheckoutV2Enabled: Boolean
get() = remoteConfig.getBoolean("checkout_v2_enabled")
}
// In CheckoutScreen
if (featureFlags.isCheckoutV2Enabled) {
CheckoutV2Screen()
} else {
CheckoutV1Screen() // safe fallback
}
When checkout_v2_enabled is set to false in the Firebase console, the next Remote Config fetch (typically every hour) disables the feature for all users — no Play Store submission required.
Key Takeaways
| Practice | Rule |
|---|---|
| Staged rollout | Start at 1%; expand only if crash rate < threshold (e.g., 0.1%) |
| Halt early | If crash rate spikes, halt immediately — don't wait for user complaints |
| Hotfix branching | Branch from production tag, not main |
| Hotfix versionCode | Increment last digit (3.2.0 → 3.2.1); CI tags trigger the pipeline |
| Kill switch | Remote Config bool per feature; set false to disable without release |
| Merge hotfix back | Always merge hotfix into main and develop to avoid regression |