← Back to blog

Release build works locally but fails on Google Play

If a release build works when you sideload it but crashes, opens to a white screen, or does nothing when a tester installs it from Google Play, the build is usually fine. What changed is everything around it. Google Play generated different APKs from your bundle, signed them with a different key, and delivered them to a device you have never held.

The first time this happened to me I lost a whole afternoon rebuilding from clean, clearing caches, and reading logcat on my own phone, where nothing was wrong. The problem was somewhere else entirely. These are the three causes I now check in order, with the details verified against the Android and Play Console documentation as of October 2026.

Why does a release build work locally but fail from Google Play?

The build Google Play delivers is not the file you tested. Play generates split APKs from your app bundle for each device, signs them with the app signing key instead of your upload key, and serves them to devices and Android versions you have never run. Any of those three differences can break an app that worked when sideloaded.

Before going further, answer one question honestly: was the build on your phone the release variant? Running from Android Studio installs the debug variant unless you change it in the Build Variants window, and Google recommends enabling its code optimizer only for release builds. If you have only ever run debug, the first cause below will reproduce on your own desk the moment you install a real release build. That makes it the cheapest one to rule out.

How do you fix a release build that R8 broke?

Find the class or member named in the crash, then add a keep rule that tells R8 not to remove or rename it. Google's troubleshooting guide says crashes after optimization are typically broken reflection, which shows up as a ClassNotFoundException, NoSuchMethodException or NoClassDefFoundError. Keep the rule as narrow as you can.

R8 is the optimizer that runs when a release build has minification turned on. According to Enable app optimization with R8, it removes code it cannot see being used, shortens class and method names, and inlines and merges code. In most existing projects the switch looks like this:

// app/build.gradle.kts
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

R8 follows direct calls in your code. It cannot follow a class looked up by name, a method found through reflection, or a call that arrives from native code through JNI. A library that works in debug can have its entry points removed in release for exactly that reason. Full mode, which makes stricter assumptions about reflection, has been the default since Android Gradle Plugin 8.0, so a project upgraded from an older plugin can start failing with no change to its own code.

The fix is a keep rule in proguard-rules.pro. On Android Gradle Plugin 9.3 and higher, with the newer optimization block, rules go in a .keep file under src/main/keepRules/ instead. Two shapes cover most cases:

# A class that is only ever loaded by name, for example
# Class.forName("com.example.app.PluginImpl")
-keep class com.example.app.PluginImpl {
    <init>();
}

# Fields that a serialization library reads by reflection,
# on model classes your code already references
-keepclassmembers class com.example.app.model.** {
    <fields>;
}

The keep rule reference recommends -keepclassmembers for most situations and warns that a bare -keep on a wide package pattern blocks optimization for everything it matches. Resist the urge to keep your whole package just to make the crash go away.

Before writing rules for a third-party library, check whether it already ships its own. R8 writes the merged rules from every module and library to app/build/outputs/mapping/configuration.txt, so you can see what is already being applied.

Which SHA-1 fingerprint do you register when you use Play App Signing?

Register the app signing key certificate, because that is the certificate on the build users install. Play Console Help says you must register the Google-held app signing key fingerprint with your API providers, not just your local upload key. Copy the SHA-1 or SHA-256 from the Play app signing page in Play Console.

This is the cause that feels most like a haunting. You upload the bundle signed with your upload key. Play verifies it, then signs the APKs it delivers with the app signing key. Anything that identifies your app by package name plus certificate fingerprint now sees a certificate it was never told about. The Use Play App Signing help page names Google Maps, OAuth and Facebook Login as examples. Google's client authentication guide for Play services adds Google Sign-In, which needs the SHA-1 to create an OAuth client.

The symptom is specific: the feature works on the build you sideloaded and is refused on the build from Play, with no crash in your own code. To fix it:

  1. In Play Console, open Protected with Play, then Play Store distribution, then Go to Play app signing. Older guides call this Setup, then App signing or App integrity.
  2. Scroll to the App signing key section and copy the SHA-1 or SHA-256 fingerprint.
  3. Paste it into the provider's console. In Firebase, that is Project settings, then your Android app, then Add fingerprint.

Add the new fingerprint next to the existing ones. Do not replace your debug and upload fingerprints, or your local builds stop working instead. One newer wrinkle: the help page says that apps using quantum-ready hybrid signing have three app signing keys, and that you must register the fingerprints for each of them.

The same fingerprint belongs in your assetlinks.json file if you use Android App Links, which is the root of the problem in my post on app links that open in the browser. To see which certificate signed a file you have on disk, run:

keytool -printcert -jarfile app-release.aab

That prints your upload certificate. It will not match the build Google Play distributes, and that mismatch is the whole point. If you are unsure which local keystore is which, the Play Console checks I run before publishing walk through comparing them.

How do you test the exact build Google Play delivers?

Upload the bundle to the internal testing track and install it from the Play Store on a second device. Internal testing supports up to 100 testers, and a new bundle is available to them within minutes. Internal app sharing is faster still, but it re-signs your build with a separate test certificate.

Play Console Help says you can start an internal test before app setup is complete, and that internal tests might not be subject to the standard policy and security reviews. The first time you publish a test, the opt-in link can take several hours to become available. After that, the loop is short enough to use for every release.

Internal app sharing is the lighter option. You upload a bundle or APK to the internal app sharing page and get a link. Version codes can be reused, debuggable builds are allowed, up to 100 users can download through one link, and links expire 60 days after upload. Testers enable it by tapping the Play Store version number seven times in the Play Store settings.

The catch matters for this exact bug. Internal app sharing re-signs every upload with its own Internal App Sharing key, not your app signing key. A fingerprint problem will therefore look broken there even after you fix it for production, unless you also register the test certificate, which you can download from the Internal test certificate section of the same page. For signing bugs, use the internal testing track.

Two more options keep you off Play entirely. You can download the device-specific APKs that Play generated from the app bundle explorer and install them with adb install-multiple *.apk. Or you can recreate the split locally with bundletool, the same tool Play uses:

bundletool build-apks --connected-device --bundle=app-release.aab --output=app.apks
bundletool install-apks --apks=app.apks

Do not sideload a single split by hand. Android's app bundle documentation notes that a sideloaded install missing a required split APK fails on all Google-certified devices and on Android 10 and higher.

Devices and Android versions you have never touched

The third cause is the plainest. Your minSdk is a promise that the app runs on every Android version from that number up, and Play takes you at your word. If you have only tested on one recent phone, most of that promise is untested.

Two things help. First, keep at least one older device, or an emulator at your minSdk, and install from the internal track on it before every promotion. Second, read the pre-launch report. When you upload a bundle to a closed or open testing track, Google runs it on test devices that crawl the app for several minutes and report stability and compatibility problems. If your account needs the closed test with 12 testers, you get that report as a side effect.

After launch, the same failures show up as per-device crash rates, which is where the Android vitals bad behavior thresholds come in.

Read the crash from the Play-installed build

Whatever the cause, stop reading logs from the build that works. Connect the device running the Play-installed version and pull its crash buffer:

adb logcat -b crash

The answer is almost always in the first crash line. If the stack trace is full of single-letter class names, that is R8's renaming. The mapping file that reverses it is bundled into your app bundle automatically, and Google's troubleshooting guide says Logcat deobfuscates traces by itself from Android Studio Otter 3 Feature Drop with Android Gradle Plugin 9.0. On older setups, use retrace:

$ANDROID_HOME/cmdline-tools/latest/bin/retrace app/build/outputs/mapping/release/mapping.txt trace.txt

Save a copy of mapping.txt for every release you publish, because the next build overwrites it.

Where IOn Emit fits

IOn Emit is the Windows desktop app I built for publishing to Google Play. It supports all four release tracks with staged rollout control, so pushing a bundle to internal testing first is the same few clicks as pushing to production. It is a publishing tool, so the R8 rules and fingerprints above still live in your project and in Play Console.

The checklist I use now

  1. Run the release variant locally. If it crashes on your own device, it is R8. Add a narrow keep rule.
  2. Register the app signing key fingerprint. Copy the SHA-1 and SHA-256 from the Play app signing page into every API provider and into assetlinks.json.
  3. Upload to the internal testing track. Never treat a local release build as the final check.
  4. Install from the Play Store on a second device. Make it an older one, at or near your minSdk.
  5. Pull logcat from that device. Read the first crash line before changing any code.
  6. Promote only after it launches there. Then release in stages, not to everyone at once.

This post is part of our app publishing guides for indie Android developers.

Try it free
IOn Emit - Free to start

Publish to Google Play from a Windows desktop app: a 9-step pre-flight wizard, pre-publish validation, AI descriptions, a screenshot studio, and a 100-point ASO score. The publishing suite is free. Pro is $19/mo.

→
Keep reading
IOn Emit 3 Play Console Checks Before You Publish an Android App Read → IOn Emit Android App Links Open in the Browser? Fix assetlinks.json Read → IOn Emit Android Vitals Bad Behavior Thresholds, Explained (2026) Read →
Reading us on Google?
Add The IOn Project as a preferred source

One click on Google’s preferences page, and our articles show up more often in your Top Stories, AI Overviews, and AI Mode.

→