1. Two fields, two completely different jobs. Google's versioning guide defines them separately: versionCode is "a positive integer used as an internal version number. This number helps determine whether one version is more recent than another, with higher numbers indicating more recent versions. This is not the version number shown to users; that number is set by the versionName setting. 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." versionName is "a string used as the version number shown to users... The versionName is the only value displayed to users." Play Console help adds the operability detail developers trip over first: versionCode is "a 32-bit integer that consistently increases with each new release" while versionName is "a string that represents the user-facing version number, for example, '1.0.0', '1.1 Beta', '2.0'." Marketing can rename 4.9 to 5.0 freely; the moment the integer fails to increase, the upload dies. (§https://developer.android.com/studio/publish/versioning / §https://support.google.com/googleplay/android-developer/answer/17367361)
2. Play's upload gate is one comparison, and it is strict greater-than. The official update requirements list three hard conditions, in this order: "The package name of the updated Android App Bundle must be the same as the current version. The version code must be greater than the current version... The updated app bundle must be signed with the same signature as the current version." There is no "equal is fine if the binary differs" path, and there is no argument to make in review: the check is mechanical and happens before a human or a policy bot ever sees the release. (§https://support.google.com/googleplay/android-developer/answer/9859350)
3. A versionCode is never reusable, and the pool belongs to the app, not to the track. The versioning guide is blunt: "You can't upload an APK to the Play Store with a versionCode you have already used for a previous version." In the console that surfaces as the familiar rejection "Version code X has already been used." Because the counter is attached to the application (the package name), internal testing, closed testing, open testing and production all draw from the same sequence; to put the same build on a second track you add it "from library" rather than uploading it again. A distinct package name - a staging app, a white-label flavour with its own applicationId - has its own sequence, which is exactly why a staging app is the right place to burn codes while debugging. (§https://developer.android.com/studio/publish/versioning)
4. The ceiling is 2,100,000,000, and hitting it is not a support ticket, it is an end state. Both documents state the same number: "The greatest value Google Play allows for versionCode is 2100000000." The console page spells out the consequence: "If your app's versionCode reaches or exceeds the maximum limit, you can no longer be able to upload new APKs or app bundles to Google Play for that application. This means no more bug fixes, new features, or compatibility updates for your users." Google's only stated remedy is that "Google Play may be able to provide support" - a maybe, not a reset button. For a business that earns from live apps, the number is therefore an asset with a shelf life, and it must be spent like one. (§https://support.google.com/googleplay/android-developer/answer/17367361)
5. Three numbering habits burn the range faster than developers expect. Unix timestamps in seconds are already past 1.79 billion and climb on every build; a global CI run counter shared across branches, flavours and sibling apps races forward on failed pipelines; and "build number = commit count" explodes the moment a monorepo history is rewritten. Play's own mitigation list is: "Adopt a sustainable versionCode strategy: implement a versionCode generation strategy that ensures a gradual and consistent increase, and avoids rapid consumption of the available range. Monitor versionCode usage: regularly track your app's versionCode progression to anticipate when it might approach the limit. Implement automated checks: integrate checks within your build and release processes to warn or prevent the use of excessively high versionCode values." (§https://support.google.com/googleplay/android-developer/answer/17367361)
6. Codes are consumed by upload, not by publication. Uploading to internal testing, parking a bundle in the library, or creating a draft release that is later discarded all leave the number spent - "Add from library" exists precisely because the same binary can be re-attached to a later release but never re-created. Anyone who "pushes a build to the console to see whether it passes" pays one version number per attempt; on a long-lived app with a fast pipeline, that is how a comfortable gap becomes a crisis years later.
7. Halting is not a rollback, because Android refuses downgrades. The system-level rule quoted in point 1 (a device will not install a lower versionCode than the one already installed) is why there is no console button that returns users to last week's build. Google's staged rollout guidance says the same thing operationally: "If you find an issue with your existing app bundle, create and roll out a new release with a fixed app bundle." So the only true recovery from a bad production build is a new bundle, with a versionCode higher than the bad one, shipped as fast as review allows - which is precisely why the emergency slot in your numbering scheme has to exist before you need it. (§https://support.google.com/googleplay/android-developer/answer/6346149)
8. Staged rollout is an update-only tool, and it never advances by itself. "You can release an app update to production and test tracks using a staged rollout. With a staged rollout, your update reaches only a percentage of your users, which you can increase over time. Staged rollouts can only be used for app updates, not when publishing an app for the first time." And the trap that leaves releases stuck at 5% for a week: "your app's staged rollout percentage won't increase automatically." Whoever owns the release has to come back and raise the percentage; nothing on Google's side will do it. Users are also not told they received a staged update - "Users won't be notified if they receive a version of your app in a staged rollout" - so feedback on a staged release has to be pulled from vitals and support, not from reviews. (§https://support.google.com/googleplay/android-developer/answer/6346149)
9. What halting a staged rollout actually changes. "If you discover an issue, you can halt a staged rollout to help minimize the number of users who experience the issue with your app. When you halt a staged rollout, no additional users will receive the app version in your existing staged rollout. Users who already received the app version in your staged rollout version will remain on that version." Two consequences follow that people miss: resuming does not reshuffle the audience - "When you halt and then resume the rollout of your release, you'll be affecting the same set of users" - and starting a new release before the previous one finished reuses that same cohort: "When you do a staged rollout of a new release before completing the rollout of the previous release, the new release will use the same group of users as the previous release (depending on the percentage of the rollout)." Your fix is validated by the same unlucky 5%, not by a fresh sample. (§https://support.google.com/googleplay/android-developer/answer/6346149)
10. Halting a fully rolled-out release: what it does, and what it does not. "If you discover a critical issue with your app, the halt rollout feature minimizes disruption for users by preventing new and existing users from installing, or updating to the affected version of your app. When you halt a full release, the previous version of your app will become available to new users, and anyone not using the halted version of your app." That is a serving change, not an uninstall: "The halted version will no longer be served to users," but devices that already took it keep it, again because of the downgrade protection. Three limits are written into the same page: it works "on any track, excluding internal test tracks"; "You cannot halt your first release on a track"; and "If your previous release has a policy violation, it cannot be used in place of your halted release." A later resume can go to any percentage, which is the right way to re-introduce a fixed build gradually. (§https://support.google.com/googleplay/android-developer/answer/16285429)
11. One outstanding release per track - and this is what blocks a hotfix at the worst moment. "You cannot create a new release when you have outstanding releases. Roll out any staged releases to 100%, or remove changes on the Publishing overview page and discard any unpublished releases first." An abandoned 20% rollout, a half-finished review submission, or a managed-publishing hold left sitting on the production track will stand between you and the emergency build. The habit that prevents this: after every release, close the loop - roll to 100%, or remove and discard - and note the state in the release log so the next person (or the next you) is not debugging a console that refuses to open a new release. (§https://support.google.com/googleplay/android-developer/answer/9859348)
12. A numbering scheme that survives ten years of releases. Reserve fixed-width bands so arithmetic stays readable and the ceiling stays far away: versionCode = major * 100000 + minor * 1000 + patch * 10 + hotfix, which allows majors 1 through 20999, one thousand minors, and ten patch/hotfix slots each. Keep the working range visibly below 2.1 billion in your build script and fail the build (not the upload) when the computed value crosses a soft threshold you set at, say, 1.5 billion. Give CI authority to increment only on upload, never on every commit; run every exploratory build under the staging applicationId so its codes are spent on a different sequence; and record the shipped versionCode in the release notes so the number is auditable rather than tribal knowledge.
13. Pre-upload checklist that catches all of the above in one pass: (a) the versionCode is strictly greater than every code ever uploaded for this app, on every track - not just the track you are targeting; (b) versionName is display text only and may repeat or jump freely; (c) package name is unchanged and the bundle is signed with the live signing key - changing the app signing key means "Internal and closed track users who already installed your app will no longer receive updates. These users must uninstall and reinstall the app to receive updates"; (d) the upgrade path is tested on a device that already has the previous version installed, because a clean install proves nothing about an update; (e) the number is checked against the 2,100,000,000 ceiling and against your own soft threshold; (f) no other release is left outstanding on the target track; and (g) an emergency code slot is still free in the current band. Every one of these is a field or a state, not a judgement call - which is why the whole list can live in CI and in the release checklist instead of in someone's memory. (§https://support.google.com/googleplay/android-developer/answer/9859348)