What It Costs to Maintain an App: Nine Years of Forced Updates on One Android App
Not a percentage of the build cost. The updates Google Play required of one app, what came with them, and what it took, read from nine years of git history so you can price it at your own rate.

How much does it cost to maintain an app? The short answer: on Android, plan for one target API raise a year, a Google Play Billing Library upgrade before your installed version's support deadline, and the follow-up work each one causes. On one app, the follow-up was the expensive part. The rest of this piece is the record behind that.
The Android app I wrote in 2017 has had its target API level raised eleven times (counting one raise that was rolled back the next day and redone months later). Its Billing Library has been through six major versions. For three and a half years, it had no tests.
Search for app maintenance cost and you get a percentage of the original build budget, usually from a company selling maintenance. I can't check that ratio, and it doesn't tell you what the money buys. So here is the record instead, from the app's own git history.
The app is a client's, live on Google Play, so I don't name it or link the code. I built it in 2017 and 2018, other developers maintained it from 2019 to 2025, and I came back to it in 2026.
The required updates, year by year
Commits per year, all authors, merges included:
| Year | Commits | Target API change | Billing Library |
|---|---|---|---|
| 2017 | 505 | 25 (first build) | |
| 2018 | 1,319 | 26, then 27 two days later | |
| 2019 | 21 | ||
| 2020 | 37 | 28, then 29 | 3.0.0 (first added) |
| 2021 | 108 | 30, back to 29 the next day; 30 again in October | 4.0.0 |
| 2022 | 254 | 31 | 4.1.0, then 5.0.0 |
| 2023 | 196 | 33, then 34 a week later | 6.0.1 |
| 2024 | 231 | 7.0.0 | |
| 2025 | 101 | 35 | 7.1.1, back to 7.0.0 a week later |
| 2026 | 658 (to late October; mostly fixes and features since I came back) | 36 | 9.1.0 |
The two updates Google Play enforces with deadlines did not arrive one a year. 2019 and 2024 had no target API change; 2018, 2020, 2021 and 2023 each had two.
The 2019 and 2020 rows are almost empty. Then 2022 and 2023 carry three target API raises between them, from 30 to 34, skipping 32. On this app, the years with the least activity were followed by the years with the most catching up.
Why Google Play requires these updates
Neither column is optional for an app that wants to keep shipping.
The target API level is the Android version an app says it was built and tested against. Google Play's target API level requirements currently require new apps and app updates to target Android 16 (API level 36).
Existing apps that don't update face a different cost. They must target Android 15 (API 35) or higher to stay available to new users on newer Android versions, and the page says older ones "will only be available on devices running Android OS that are the same or lower than your apps' target API level."
The Billing Library has its own clock. Google's deprecation FAQ says: "All versions of the Google Play Billing Library have a two-year deprecation cycle." After a version's deadline, new apps and updates must use a supported one, with an extension available on request. When you next upgrade depends on which version you are on; the FAQ's table lists each one's deadline.
Play requires the version number; the removed libraries, rewritten callbacks and tests below came with meeting it. I walked through what a target API raise breaks in the target API level checklist; this piece is about what it costs.
What came with the raises: libraries, permissions and tests
In October 2022, the app moved from target API 30 to 31. The commit that did it also removed two libraries, JobDispatcher and LeakCanary, and patched a third-party SDK's manifest by hand.
The same day, another commit removed JUnit, Espresso, Mockito and every test file in the app. I wasn't on the app then, and the commits don't say why. They only show that the two changes went out together.
A year later the pattern repeated at a smaller size. One September 2023 commit raised the target to 34, moved Billing from 5.0.0 to 6.0.1, removed the storage permissions the app no longer needed, and dropped several unused dependencies.
No test file was added for the next three and a half years. When I came back, any change I made would ship with nothing in the repo to catch a regression. So before the Billing upgrade, I started putting tests back: the first, in May 2026, covered one settings class, with a seam added so it could be tested without the Android runtime. By the time I touched billing in August, there were 29 unit test files, and the billing fix added the thirtieth. Today there are 92 test files, unit and instrumented.
That is a cost the version tables don't show. The raise is one line; whether you can change the app safely afterwards depends on what else is in the repo. I wrote about inheriting an app in that state in Before You Rewrite an Inherited App, Stabilize It.
One required update, measured: Billing Library 7 to 9
When I came back in 2026, the app was on Billing Library 7.0.0, and the FAQ's table showed version 7's deadline weeks away, so it had to move. I upgraded it in August.
The upgrade itself was small. Four files, 14 lines added and 6 removed:
client = BillingClient.newBuilder(MyApplication.getInstance())
- .enablePendingPurchases()
+ .enablePendingPurchases(PendingPurchasesParams.newBuilder()
+ .enableOneTimeProducts()
+ .build())
+ .enableAutoServiceReconnection()
.setListener(this)
.build();
The bump that compiled: Billing 8.0.0 removed the no-argument enablePendingPurchases() and changed the queryProductDetailsAsync() listener to take a QueryProductDetailsResult.
Both changes are in the Billing Library release notes for 8.0.0. Skipping version 8 doesn't skip its breaking changes; going from 7 to 9 meant taking them anyway.
The second commit, the same day, was the expensive one. I had opted into enableAutoServiceReconnection(), which the release notes describe as handling reconnections to the billing service automatically. That was my choice, not Play's requirement.
It moved the setup and disconnect callbacks onto the library's own timing. The old code drained a plain ArrayList of waiting callbacks with no lock, while app code could still add to it. The fix comment in the code puts it plainly: a concurrent add() could race the iterator and throw ConcurrentModificationException. I fixed it by draining under a lock, and wrote a test that adds callbacks while the drain runs.
| Commit | Files | Lines |
|---|---|---|
| Version bump and API changes | 4 | +14 / −6 |
| Callback drain under a lock, with a test | 2 | +199 / −29 |
The cost of a required update is rarely the version number. Here the follow-up added about fourteen times as many lines as the bump, 164 of them a test.
Both commits were written in one day; the merge followed about two weeks later. A library upgrade that compiles is the start. The cost is in what the new version changes about timing, threading or results, and you find that by testing it, which is easier if the tests already exist.
How much does it cost to maintain an app per year?
I can't give you a dollar figure for your app, and I would distrust anyone who gives one without seeing it. What this history gives you is the list of things that will happen on Google's schedule, whether or not you plan for them.
To price it, take your developer's rate and estimate each row below for your app: how many SDKs it has, whether it sells anything through Play, and whether it still has tests to run after each change. Also budget for what isn't on this list: other SDK upgrades, testing on new Android versions, crash monitoring, and the iOS app if there is one. If yours has already missed a few years, an app that hasn't shipped in a year usually has more waiting than the code shows, and working through that list is what I do if you want help.
From this app's record, on Android alone:
| Work to budget for | How often | What came with it here |
|---|---|---|
| Target API raise (Play deadline) | Yearly on Play's schedule; here, zero to two a year | A one-line change, plus whatever the new Android version changes: removed libraries, removed permissions |
| Billing Library major (Play deadline) | Before your installed version's deadline | A small bump and a larger follow-up: 4 files, then 2 files with a 164-line test, in one day |
| Build toolchain (your choice of version) | Alongside both | The Gradle wrapper moved five times in four months of 2025, twice in the Billing 7.1.1 change and its rollback |
| Tests to check the change (not required by Play) | Before each of the above | None existed for three and a half years; rebuilding started with one settings class |
| Catch-up after a quiet year | After a year with no updates | Here, 2022 and 2023 took the target from 30 to 34, alongside a Billing major each year |
Keep reading
- Google Play Target API Level Requirement: A Checklist for an App Nobody Has TouchedWhat Play Help requires, what changes when you raise targetSdk to 36, and the order that keeps a neglected app shippable.
- Flutter vs React Native: How I Choose, and When I Go NativeA decision guide, not a benchmark: one production Flutter app, React Native work that was brownfield, SDK and sample code, and the cases where I would not use either.
- Claude Code in Android Studio: The Editor Is the Easy PartThe plugin gives the agent diffs, your selection and lint. The phone is reached from the terminal, and that is where my agents' habits, my screenshots and my own guard hook surprised me.