Downgrading an installed Android app with adb install -d
You installed build 142 on a test phone, a regression showed up, and now you want build 141 back. You run adb install app-141.apk and get INSTALL_FAILED_VERSION_DOWNGRADE. Android refuses to replace an installed package with one that has a lower version code, and it does not care that you are the developer.
This tutorial is for QA engineers, Android developers and anyone who runs test devices and needs to move an app backwards without wiping the phone. The short answer to the Stack Overflow question this error usually comes from is: add -d (and usually -r), but know that on a normal user-build device it only works when the app you are installing is debuggable. I will show where that line sits, what to do when you hit it, and how to check the result.
Everything here is for apps and devices you own or are authorised to test. I am not covering ways around the platform’s protections for other people’s apps. If a build is not yours to test, this is not the tutorial for it.
what you need
- a computer with adb from Google’s platform-tools (free, download from the Android developer docs)
- an Android device or emulator with USB debugging enabled and authorised for your computer
- the older APK, signed with the same key as the installed version (or an AAB-derived set of split APKs)
- the package name of the app (for example
com.example.shop) - optional:
aapt2from Android build-tools, to read version codes out of an APK file - optional: a way to back up app data first, because downgrades can break data written by a newer schema
Cost is zero for the tooling. If you do not have a spare phone, I cover the one rental option I use near the end. If adb itself is still fuzzy, read what the adb client, server and daemon actually are first, since half of the “device not found” problems below come from a confused server process.
step by step
Step 1: confirm the device is visible
Action: list connected devices.
adb devices -l
Expected output: one line per device, ending in device, with model and transport info.
R58M12ABCDE device usb:1-1 product:beyond1 model:SM_G973F device:beyond1 transport_id:1
If it breaks: unauthorized means you need to accept the RSA prompt on the phone. offline usually clears with adb kill-server && adb start-server. Nothing listed at all is normally a cable or driver problem on Windows.
Step 2: read the installed version code
Action: ask the package manager what is installed. On macOS or Linux use grep, on Windows PowerShell use Select-String.
adb shell dumpsys package com.example.shop | grep -E "versionCode|versionName"
adb shell dumpsys package com.example.shop | Select-String "versionCode|versionName"
Expected output: something like versionCode=142 minSdk=24 targetSdk=34 and versionName=2.8.0.
If it breaks: empty output means the package name is wrong or the app is not installed for the current user. Run adb shell pm list packages | grep shop to find the real name. Note that versionCode is the integer Android compares. versionName is only a label for humans, which is why a “lower” version name with a higher code still counts as an upgrade. Google documents the distinction in its app versioning guide.
Step 3: read the version code of the APK you want to install
Action: check the older file before you push it, so you know it really is lower.
aapt2 dump badging app-141.apk | grep -E "package:|versionCode"
Expected output: a line like package: name='com.example.shop' versionCode='141' versionName='2.7.4'.
If it breaks: if aapt2 is not on your path, it lives under Android/Sdk/build-tools/<version>/. If the code is equal to or higher than the installed one, you do not need -d at all, a plain adb install -r will do. If the package name differs, you are installing a different app and downgrade flags are irrelevant.
Step 4: reproduce the failure
Action: try a normal replace install so you see the exact error.
adb install -r app-141.apk
Expected output:
Performing Streamed Install
adb: failed to install app-141.apk: Failure [INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade detected: Update version code 141 is older than current 142]
If it breaks: a different error means a different problem. INSTALL_FAILED_UPDATE_INCOMPATIBLE means the signing key differs from the installed app, and -d will not help with that. INSTALL_FAILED_INSUFFICIENT_STORAGE is what it says. Do not skip this step, because knowing which error you have saves you from trying the wrong fix.
Step 5: install with -d
Action: add the downgrade flag, and keep -r so it replaces the existing package.
adb install -r -d app-141.apk
Expected output:
Performing Streamed Install
Success
The -d flag is documented in the adb install options in Google’s adb reference as allowing version code downgrade. Since Android already keeps the app’s data directory during a replace install, your login state and local files stay in place. That is convenient and also the main risk, which I get to in the pitfalls section.
If it breaks: if you still see INSTALL_FAILED_VERSION_DOWNGRADE with -d present, you have hit the rule that matters most in this whole article. As of September 2026, on current Android versions a user-build device only honours the downgrade request when the package being installed has android:debuggable="true" in its manifest (or the device itself is a userdebug or eng build, or an emulator image with debuggable set). A release-signed production APK on a retail phone is refused even with -d. Go to step 7 for your options.
Step 6: verify the downgrade took effect
Action: read the version code again and launch the app.
adb shell dumpsys package com.example.shop | grep versionCode
adb shell monkey -p com.example.shop -c android.intent.category.LAUNCHER 1
Expected output: versionCode=141, then a line saying Events injected: 1.
If it breaks: if the app crashes on start, the newer version probably migrated its local database or preferences and the older code cannot read them. Capture the crash with logcat, filtering by your app. My logcat filtering guide covers the tag and priority syntax, and for a crash that also drags the system with it, capturing and reading an adb bugreport gives you the whole picture in one zip.
Step 7: when -d is refused, pick a clean path
Action: choose one of three options depending on what you control.
- rebuild the older version as a debug or QA variant, with a debuggable flag, then use
adb install -r -dagain. This is the right answer when you own the source - uninstall and reinstall:
adb uninstall com.example.shopfollowed byadb install app-141.apk. This deletes app data, so it is fine for a test device and bad if the data matters - keep data while removing the package:
adb uninstall -k com.example.shopremoves the app but keeps its data and cache directories, then a fresh install of the older APK picks them up again. Whether the old code tolerates that data is on you
Expected output: Success from each command.
If it breaks: adb uninstall -k behaviour varies by Android version and by whether the app is a system app, so test it on a spare device before you rely on it. For an app that ships in the system image, you cannot uninstall it fully, and adb shell pm uninstall -k --user 0 <package> only removes it for the current user.
Step 8: handle split APKs and app bundles
Action: if the app was installed from a Play bundle, it may consist of a base APK plus splits, and you need install-multiple.
adb install-multiple -r -d base-141.apk split_config.arm64_v8a-141.apk split_config.xxhdpi-141.apk
Expected output: Success.
If it breaks: every split must come from the same build and be signed with the same key. Mixing a 141 base with 142 splits gives a signature or version mismatch error. Extract the set with bundletool if you are starting from an .aab.
Step 9: script it so nobody repeats the check
Action: wrap the version comparison and install into one script that refuses to run if the target is not lower.
#!/usr/bin/env bash
set -euo pipefail
PKG="$1"; APK="$2"; SERIAL="${3:-}"
ADB="adb ${SERIAL:+-s $SERIAL}"
installed=$($ADB shell dumpsys package "$PKG" | grep -m1 -oE 'versionCode=[0-9]+' | cut -d= -f2)
target=$(aapt2 dump badging "$APK" | grep -oE "versionCode='[0-9]+'" | grep -oE '[0-9]+')
echo "installed=$installed target=$target"
if [ "$target" -lt "$installed" ]; then
$ADB install -r -d "$APK"
else
$ADB install -r "$APK"
fi
Expected output: the two version codes, then Success.
If it breaks: on Windows run it under Git Bash or WSL, or port it to PowerShell. If you run several devices, always pass the serial, because plain adb with two devices attached fails with more than one device/emulator.
common pitfalls
- assuming -d works on any APK: it works on debuggable builds, emulators and eng/userdebug devices. It does not turn a locked retail phone into a place where any release build can be rolled back. Plan your QA pipeline so that downgrade candidates are debug or QA-signed variants
- ignoring data compatibility: a replace install keeps the app’s data. If version 142 ran a database migration, version 141 may crash or silently misread it. Back up, or clear data with
adb shell pm clear com.example.shopafter the downgrade when you do not need the old state - signing key mismatch: a QA build signed with your debug key cannot replace a Play-signed release. You will see
INSTALL_FAILED_UPDATE_INCOMPATIBLE, and no flag fixes that. Uninstall first, or sign the older build with the matching key if it is yours - using -d against production users:
-dis an adb developer tool. It has no place in a real rollout. Real users get rollbacks by publishing a new build with a higher version code that contains the old code, which is how the Play Store expects it - forgetting the target device: with two phones attached, an unqualified
adb installerrors out, and one careless-stypo can land the old build on the wrong device. Print the serial in your script output
scaling this
At 10 devices, a shell loop over adb devices output is fine. Pass -s per serial, run installs in parallel with & and wait, and log Success or failure per serial. I would still check the version code per device, because a half-updated bench is where “works on my phone” comes from.
At 100 devices, the loop stops being the problem and inventory does. You need to know which device holds which build, which are debuggable-capable, and which are offline. This is the point to look at a proper device farm, and my list of open source Android device farm tools compares the options I have looked at. Build an artifact store with one folder per version code, so a downgrade is a lookup, not a hunt through chat history.
At 1000 devices, you are not typing adb commands anymore. You are running a fleet manager with health checks, rollout rings and automatic rollback through your own update channel, with adb only as the escape hatch for a stuck unit. -d becomes a niche tool for the lab.
If you have no spare hardware, cloudf.one rents real Android phones in Singapore on dedicated hardware, each with a persistent Singapore mobile IP, controlled from the browser. It is Singapore only. It has no self-service adb: adb and Appium reach a rented phone only through a WireGuard tunnel that we set up on request (email [email protected]). It is useful when you want a clean retail-style device for a one-off check, and less useful if you need a quick unattended downgrade loop today.
where to go next
- the blog index for the full list of adb and device tutorials
- comparing the best adb GUI tools if you would rather click than type these commands
- Android RPA with adb and UiAutomator2 once your build is on the device and you want to drive it from a script
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-09-29.