SYSONLINE
SDK36 · MIN 24
KOTLIN2.0
UTC07:37:20
UP00:01
MOD · ARTICLE · CI/CDS/N · AX-PLAY-SPUBLISHED
CI/CDMay 12, 202610 MIN

You Cannot Recall an APK: Staged Rollouts and Release Automation on Google Play

A bad Android release cannot be pulled back off a phone that installed it. Staged rollouts, halts and the Publishing API decide how many phones that is.

By Rocky Elsalaymeh · Founder & Principal Consultant, Strategia-X

There Is No Undo on a Phone

Web teams roll back. A bad deploy goes out at 14:00, the previous build is serving again at 14:06, and by 14:30 the incident is a paragraph in a retro. Mobile teams inherit that instinct and it is wrong, because the artefact is not on your servers. It is on the user's phone, and Android is designed to keep it there. The versioning guide states the mechanism: "The Android system uses the versionCode value to protect against downgrades by preventing users from installing an APK with a lower versionCode than the version currently installed on their device." Google Play's staged rollout documentation states the consequence: when you halt a rollout, "users who already received the app version in your staged rollout version will remain on that version." Put those two sentences together and the operational rule follows: every user who installed a bad build can only be rescued by a *new* build with a *higher* versionCode. Play has narrowed the gap. At I/O 2025 Google acknowledged that "historically, a release at 100% live meant there was no turning back," and you can now halt even a fully rolled-out release: "a previously live, fully rolled-out version of your app will automatically take its place and become available to new and eligible users." That stops the bleeding for people who have not updated yet. It does not reach into the phones that already have the bad build. So the question that matters is not "can we roll back?" It is "how many phones will have the bad build by the time we notice?" Staged rollouts are how you choose that number in advance.

How a Staged Rollout Really Behaves

Staged rollouts are simple to switch on and easy to misread. The Play Console documentation pins down the behaviour that matters:
RuleWhat Google says
Scope"Your update reaches only a percentage of your users, which you can increase over time."
First releases"Staged rollouts can only be used for app updates, not when publishing an app for the first time."
Growth"Your app's staged rollout percentage won't increase automatically."
Halting"No additional users will receive the app version in your existing staged rollout."
Resuming"When you halt and then resume the rollout of your release, you'll be affecting the same set of users."
Fixing"If you find an issue with your existing app bundle, create and roll out a new release with a fixed app bundle."
Two of those rows are routinely misunderstood. "Won't increase automatically" means a rollout left at 5% stays at 5% forever; there is no timer, and forgotten rollouts are a common reason a fix "never reached anyone." And "the same set of users" means halting and resuming does not reshuffle the cohort: the users who were exposed stay exposed, which is what you want when you are measuring whether a fix worked. The rollout percentage is also not a sample you control demographically. Users are chosen at random, so a 1% stage on an app with a large installed base is a genuinely representative slice of devices, OS versions and markets. That is the point: the device-specific crash you cannot reproduce on your test fleet will show up in that 1% first.

Let Vitals, Not the Calendar, Promote a Release

A staged rollout is only as good as the signal that decides the next step. Play Console gives you that signal early. The Test and release overview compares a new release's user-perceived crash and ANR rates with your other releases, and "for crash and ANR rates, the data granularity will automatically default to hourly for the first few days of a new release to help you spot issues quickly." The release dashboard is blunter: "If your latest release has a significant increase in 'Crashes per 1000 devices' compared to a successful previous release, consider pausing your latest release to investigate what caused the increase." The thresholds that matter for store placement are Android vitals' bad behavior thresholds: "At least 1.09% of daily users experience a user-perceived crash, across all device models," or 0.47% for user-perceived ANRs, or 8% on a single device model, and "if your app exceeds a bad behavior threshold, it's likely to be less discoverable on Google Play." A release that pushes you over those lines is not just a bug; it is a ranking event. The ladder below is an example, not a Google prescription. Tune the stages and dwell times to your traffic: a stage has to run long enough to gather enough sessions to move the metric.
StageRolloutPromote only if
Canary1%Crash and ANR rates at or below the previous release, no new top crash cluster
Early5%Same, plus no device model trending toward the 8% per-device threshold
Broad20%Same, plus ratings and uninstalls flat against the last release
Majority50%Same, plus backend error rates and support tickets flat
Complete100%Same, plus nothing new in pre-launch or accessibility reports

Automate the Ladder with the Publishing API

Clicking through Play Console for every stage does not scale, and it produces no audit trail. The Google Play Developer Publishing API does the same work as a transaction. Changes are grouped in an *edit*: edits.insert opens one ("when you first create an edit, the edit is a copy of the current deployed state of the app"), edits.tracks.update changes the release on a track, and edits.commit publishes. Two warnings from the docs belong in your pipeline's README. First: "If anyone makes any changes to the app through the Google Play Console while you have an edit in progress, your edit is discarded." Second, edits.commit defaults to CANCEL_IN_REVIEW_AND_SUBMIT, "which will cancel the changes in review and then send all the changes for publishing", so an automated promotion can silently cancel a listing change a colleague submitted that morning. Set it explicitly. A staged release is a track update with a userFraction and a status. The tracks guide uses exactly this shape: start an "inProgress" release at a fraction, set it to "halted" to stop, and "if you later decide to resume a halted release you can do so by setting its status back to 'inProgress'." Per the API reference, the fraction must satisfy 0 < fraction < 1 and "can only be set when status is 'inProgress' or 'halted'"; completing the rollout is a status change to "completed", not a fraction of 1. Most teams drive this from Gradle. Gradle Play Publisher describes itself as "Android's unofficial release automation Gradle Plugin" that can do "anything from building, uploading, and then promoting your App Bundle or APK to publishing app listings and other metadata." Read its README before depending on it: it states that "issues are ignored, but pull requests are not," which is a maintenance posture to plan around, not a reason to avoid it.
kotlin
// build.gradle.kts: Gradle Play Publisher
play {
    track.set("production")
    userFraction.set(0.01)                        // canary stage; 1.0 is rejected
    updatePriority.set(2)                         // in-app update priority, 0..5
    releaseStatus.set(ReleaseStatus.IN_PROGRESS)
}

// CI promotes the same artifact up the ladder once vitals gates pass:
//   ./gradlew promoteArtifact --update production --user-fraction .05
//   ./gradlew promoteArtifact --update production --user-fraction .2
//
// Equivalent Publishing API body for edits.tracks.update:
//   { "releases": [{ "versionCodes": ["4210"], "userFraction": 0.05, "status": "inProgress" }] }
// Halt:     same release, "status": "halted"
// Complete: same release, "status": "completed" (no userFraction)

Fix Forward, Fast: Update Priority and Managed Publishing

When a bad build does escape, the fix-forward release has to reach the users who are stuck on it, and those users are the ones least likely to open the Play Store. The in-app updates API is how you reach them from inside the app: flexible updates "provide background download and installation with graceful state monitoring," while immediate updates "are fullscreen UX flows that require the user to update and restart the app in order to continue using it." Which flow to use is a release decision, and Play lets you attach it to the release. "To determine priority, Google Play uses an integer value between 0 and 5, with 0 being the default and 5 being the highest priority," set through inAppUpdatePriority when the release is created, because it "can not be updated once the release is rolled out." The guide adds the detail that makes it work for incident response: "the returned priority takes into account the inAppUpdatePriority for all app version codes between the installed version and latest available version." Ship the fix at priority 5 and every user on the bad build sees it as critical, whatever version they skipped from. Finally, decouple approval from go-live. With managed publishing on, approved changes wait for you instead of being "published automatically as soon as they're reviewed and approved by Google." Note the exception Google lists: "increasing an existing staged roll-out to 100%" is not held back, so the final promotion is always immediate.
kotlin
suspend fun maybeUpdate(activity: ComponentActivity, launcher: ActivityResultLauncher<IntentSenderRequest>) {
    val manager = AppUpdateManagerFactory.create(activity)
    val info = manager.appUpdateInfo.await()
    if (info.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) return

    // Priority is set per release in Play (0..5) and aggregated across skipped versions.
    val type = if (info.updatePriority() >= 4 && info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
        AppUpdateType.IMMEDIATE      // fix-forward for a bad build: block until updated
    } else {
        AppUpdateType.FLEXIBLE       // routine update: download in the background
    }
    manager.startUpdateFlowForResult(info, launcher, AppUpdateOptions.newBuilder(type).build())
}

The Release Checklist That Assumes You Will Ship a Bug

Every mobile release process should be designed around one assumption: this build contains a defect you have not found. The pre-launch report is the free first net. Reports "are automatically generated when you upload an app bundle or APK, subject to capacity within our device lab," and Google's lab devices then "automatically launch and crawl your app for several minutes." Read it before the first stage, not after. Then make the process boring: 1. Never publish straight to 100%. Every production release starts as a staged rollout; the first release on a track is the only exception Play forces on you. 2. Gate promotions on vitals, not dates. Hourly crash and ANR data exists for the first days of a release; use it. 3. Automate promotion and halting through the Publishing API or Gradle Play Publisher, with changesInReviewBehavior set explicitly. 4. Keep a fix-forward lane warm. A version-code bump, a release branch and a priority-5 in-app update path that you have rehearsed. 5. Halt first, investigate second. A halt is reversible and resumes the same cohort. Another hour at 20% is not reversible for the people who install in that hour. The teams that look calm during a bad release are not the ones with fewer bugs. They are the ones whose bug reached 1% of phones instead of all of them.
MOD · TAKEAWAYS6 POINTSSUMMARY

Key Takeaways

  1. 1Android blocks versionCode downgrades, so users who installed a bad build can only be fixed by a new build with a higher versionCode.
  2. 2Play can now halt a fully rolled-out release and serve the previous one to new users, but it cannot remove the bad build from phones that already have it.
  3. 3Staged rollouts never advance on their own, apply only to updates, and resume to the same user cohort after a halt.
  4. 4Gate each promotion on vitals: Play shows hourly crash and ANR rates for a new release’s first days, and the bad-behavior thresholds are 1.09%, 0.47% and 8% per device.
  5. 5Automate the ladder with the Publishing API or Gradle Play Publisher, and set changesInReviewBehavior explicitly; the default cancels changes in review.
  6. 6Ship fix-forward releases with in-app update priority 5 so users on the bad build see them as critical.
MOD · FAQ3 ENTRIESANSWERED

Frequently Asked

Can I roll back an Android app update on Google Play?

Not for users who already installed it. Android blocks versionCode downgrades, so the fix is a new release with a higher versionCode. Play can halt a release, including one at 100%, so new users get the previous version.

Does a staged rollout increase automatically?

No. Google states the percentage will not increase automatically. Promote it yourself in Play Console or through the Publishing API.

What userFraction completes a rollout through the API?

None. userFraction must be between 0 and 1 exclusive. Complete a rollout by setting the release status to completed.

MOD · BUILD · NEXTS/N · AX-CTA-0001READY

Ready to architect your next Android app?

ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.

ANDROID-ARCHITECT · CONSOLE
S/N · AX-1A-00001
STRATEGIA-X · ENGINEERED · IN · CALIFORNIA