Mobile architecture
How long to support old mobile app versions
The support window for old app versions is not a number you copy from another company. It is read off the tail of your own adoption curve, written down as two floors, and owned by a named person. Plus what a forced update cannot fix.
14 min read
Most pages that answer this question describe a mechanism: a version check endpoint, a remote config key, a modal with an Update button. The mechanism takes an afternoon. The hard part is the policy behind it: which versions you still serve, why that many, who is allowed to end support, and what you do for the people who cannot update.
I have run 500+ technical interviews from the hiring side over 15 years in mobile. When a system design discussion reaches old clients, the common answer is "we force an update." It is a real tool. As a strategy it usually means nobody has written the policy, and the force update is standing in for the decision.
My book Controlled Change puts the constraint in one line: "A design is incomplete if it only works when all clocks move together." A backend deploys in minutes. A mobile client may stay active for months, sit offline for weeks, or queue an operation before it upgrades. How long you support old versions is the question of how far apart those clocks may drift, and who decides when they have drifted far enough.
- The short answer: there is no universal number. The window comes from your own adoption curve, measured on the journeys that matter.
- The policy: two floors (a soft one that recommends, a hard one that blocks), a graduated path between them, and a named owner.
- The limits: what a forced update cannot fix, and why the server has to refuse unsafe operations on its own.
Why there is no standard number
Search results offer rules of thumb, such as the last few releases or the last few months. None of them can be right in general, because the inputs differ from app to app:
- How users update. On iOS, a phased release goes to a random sample of users with automatic updates on, over seven days, and Apple notes that anyone can update manually at any time. Users with automatic updates off arrive on their own schedule, or not at all.
- Who cannot update. A device that cannot run the operating system your newest build requires cannot install that build. Enterprise and managed fleets may lag on purpose. Some users have no store access at the moment you ask.
- How long work lives on the device. An operation queued offline by an old version may be sent days later, sometimes after the user has upgraded.
- What the backend has to keep. Controlled Change makes a point in its conflicts chapter that is easy to miss: a deletion tombstone can be compacted only when no supported client or sync cursor can still need it, which is one reason client support windows affect data architecture.
So two apps with identical release cadences can need very different windows. A field operations app used offline on managed devices has a long tail by nature. A consumer app with mostly automatic updates may see its tail thin out quickly. The number is a measurement, not a convention.
Read the window off your adoption curve
Every app has an adoption curve per release: a steep head while automatic updates and the phased rollout land, then a tail that thins slowly and at some point stops thinning. The window lives in the tail, and three choices decide whether you read it correctly.
- Measure the journey, not the install. Count the share of checkout attempts, trips, uploads or logins started from each version, per day. An install that never opens the app costs you nothing. A version that carries a large share of payments costs you a lot.
- Segment by platform and region. iOS and Android tails rarely look alike, and a version that is negligible overall can carry a large share of one market.
- Write the sunset rule as a number and a duration. A version becomes a candidate for end of support when its share of the journey stays under a floor you chose for a run of consecutive days you chose. Add a maximum age as a backstop, so a version with a stubborn tail still gets a decision.
The rule turns into a few lines of code over a daily series. The output is a list of candidates for the owner to sign, never an automatic removal:
// One value per day, oldest first: the share of a journey (checkout attempts,
// trips, uploads) started from one app version. Installs are the wrong unit.
typealias DailyShare = List<Double>
data class SunsetRule(val floor: Double, val consecutiveDays: Int)
fun daysBelowFloor(share: DailyShare, floor: Double): Int =
share.asReversed().takeWhile { it < floor }.size
// Candidates for the owner to sign, not removals.
fun sunsetCandidates(byVersion: Map<String, DailyShare>, rule: SunsetRule): List<String> =
byVersion.filterValues { daysBelowFloor(it, rule.floor) >= rule.consecutiveDays }.keys.toList()
// A tail that stops shrinking is a cohort that cannot or will not update.
// That is a product decision now, not a countdown.
fun tailStalled(share: DailyShare, windowDays: Int, minDrop: Double): Boolean {
if (share.size <= windowDays) return false
val recent = share.takeLast(windowDays + 1)
return recent.first() - recent.last() < minDrop
}The second function matters as much as the first. A tail that stops shrinking means a cohort is stuck: old devices, a region with a slow store, a managed fleet. At that point the countdown is over and the support decision has become a product decision. Somebody has to choose between keeping the old contract alive, degrading those users with an explanation, or blocking them, and that somebody should not be the engineer who happens to be editing the endpoint.
The chapter on version skew lists the same rule for contracts: "deprecate through observation, not assumption." Without per-version telemetry on the journeys you care about, a support window is a guess with a date on it.
Two floors, not one
A version support policy needs two published values, not one:
- The recommended version (soft floor). Below it, the app suggests an update and keeps working. Moving it costs users almost nothing, so it can move often.
- The minimum supported version (hard floor). Below it, the app blocks new journeys and sends the user to the store. Moving it takes service away from people who did not ask for a change, so it moves rarely and only on evidence.
Controlled Change does not treat the move from one to the other as a switch. Its chapter on version skew gives a graduated policy, and the steps read well as a ladder you climb one rung at a time:
| Rung | What the user sees | When it fits |
|---|---|---|
| Soft upgrade recommendation | A dismissible prompt; the app keeps working | Any version below the recommended one |
| Capability degradation with explanation | One feature is unavailable, with the reason and a path to update | The old version cannot use a new contract safely, but the rest of the app can stay |
| Deadline and reminders | A date after which the version will stop working | The version is heading below the hard floor |
| Minimum-version block | The app asks for an update before a new journey starts | Narrowly justified risk only |
| Server-side denial | A stable, explainable refusal for one unsafe operation | Always, for anything unsafe, regardless of client UI |
The client side of the two floors is small. What matters is in the comments: missing data never locks anyone out, and nobody gets walled in the middle of a journey.
struct AppVersion: Comparable, Sendable {
let major: Int, minor: Int, patch: Int
init?(_ string: String) {
let parts = string.split(separator: ".")
let parsed = parts.compactMap { Int($0) }
guard parsed.count == parts.count, (1...3).contains(parsed.count) else { return nil }
let numbers = parsed + Array(repeating: 0, count: 3 - parsed.count)
(major, minor, patch) = (numbers[0], numbers[1], numbers[2])
}
static func < (a: AppVersion, b: AppVersion) -> Bool {
(a.major, a.minor, a.patch) < (b.major, b.minor, b.patch)
}
}
// Two floors, published by the server and cached on the device.
struct VersionPolicy: Sendable {
let recommended: AppVersion // soft floor: below it, suggest an update
let minimum: AppVersion // hard floor: below it, block new journeys
}
enum UpdatePrompt: Equatable { case none, suggest, block }
func updatePrompt(installed: AppVersion,
policy: VersionPolicy?,
journeyInProgress: Bool) -> UpdatePrompt {
// No policy fetched yet: do not lock anyone out on missing data.
// The server still refuses unsafe operations from any version.
guard let policy else { return .none }
if installed < policy.minimum {
// Never wall someone in the middle of a payment or a trip.
// Finish the journey, enforce the floor at the next start.
return journeyInProgress ? .suggest : .block
}
return installed < policy.recommended ? .suggest : .none
}Mobile System Design Blueprint makes the same point in the worked example of its version skew chapter: a rider already in a car is never walled, and the floor is enforced at ride request. Enforce at the start of the next journey, not in the middle of the current one.
On Android, Google Play's in-app updates API gives you two system flows that map onto the floors: a flexible update that downloads in the background while the user keeps working, and an immediate update, a full-screen flow that requires the update and a restart before the app can be used. On iOS there is no equivalent system flow; the version check and the prompt are your own code, and the update itself happens on the App Store page. On both platforms the check only exists in versions that shipped it, so ship it long before you need it.
When a hard minimum is justified
The book is specific about when a hard minimum version is sometimes necessary: compromised credentials, unsafe policy, legal requirements, or a server contract that cannot be maintained. Its glossary defines a hard update as a minimum-version policy that blocks an old client, and adds that it may be necessary for severe risk and can deny access to users unable to update.
Notice what is not on the list: wanting to delete old code, wanting a cleaner backend, wanting fewer versions in the test matrix. Those are real costs, and they are paid with the soft floor, degradation and an honest support window, not with a block.
The book also lists what a hard minimum costs, and each item changes how you raise one:
- Users may lack store access. Give notice, a deadline and reminders before the block, not a surprise wall.
- Devices may not support the new OS. Before you raise the hard floor, check which minimum OS that version requires. If it is higher than what part of the tail can run, you are not asking those users to update; you are ending their access.
- Enterprise deployments may lag. Managed fleets update on their own schedule. Find out who they are before the deadline, not from support tickets after it.
- A bad release can lock everyone out. If the hard floor points at a version with a crash at launch, every user who updates as told gets the crash. Raise the floor only to a version that has finished its rollout and held its health numbers.
The book's release chapter ranks the move accordingly. In its list of what rollback can mean once a binary is installed, raising a minimum version comes last, as a last resort, after flags, server responses, server-side blocks and a patched binary.
What a forced update cannot fix
Treat the minimum version as a user experience, not a security control. Controlled Change is direct about it: "The server remains the authorization boundary. A client version check cannot secure a prohibited operation by itself." A forced update cannot:
- Stop a request the server accepts. A version header is sent by the client. If an old version must not perform an operation, the server refuses it, with a stable reason the client can render.
- Reach a version that never shipped the check. The same limit applies to a kill switch: a control only reaches the versions that read it. More on that in mobile app kill switch design.
- Reach a device that is offline. The block appears when the app next fetches its policy. Until then, the old version keeps running.
- Rewrite work that is already queued. The book calls queued commands historical contracts: an operation created by an old version may be sent by a newer one, so the app must keep a decoder for that payload version, migrate it without changing what the user asked, or quarantine it with a recovery path. Forcing the update does not empty the queue.
- Repair data the old version already wrote, locally or on the server.
This is also why the book lists treating app-version gates as authorization among the recurring failure modes of version skew. The block is the last rung of the ladder; the server-side denial is the rail next to every rung.
Keep the contract additive while the window is open
A long support window is affordable only if the server can keep serving old versions cheaply. That depends on how the contract changes, and the book's rules for request and response schemas are short:
- add optional fields with safe defaults;
- keep existing meaning stable, and never repurpose an old enum case;
- accept unknown fields, and represent unknown enum values explicitly on clients;
- introduce new operations when semantics change materially.
Then the window becomes a step in the sequence rather than a separate debate. The book's expand and contract pattern has six steps; the last two are where your policy is read: clients stop producing the old form after the adoption threshold, and the server removes old support only after evidence and support-window policy permit. Its migration playbook for backend API changes says the same thing from the other side: handle old clients with compatible errors or a targeted minimum version, and remove old behavior only after the measured support window.
Flags follow the same clock. A remote key that old binaries still read has to keep being served until the minimum supported version passes the last release that reads it; the full order is in mobile feature flag cleanup. The reverse is the mistake to avoid: raising the hard floor so that a flag or an endpoint can be deleted sooner.
Who decides, and where it is written
Most writing on this topic skips ownership, and ownership is what makes a support window work. Controlled Change states the requirement plainly: "Every long-lived contract needs an owner, use telemetry, a support window, and a removal plan." Its chapter on replaceable systems adds the condition that makes the owner real: "Deprecation needs authority." A platform team cannot retire an old API if product teams have no funded migration path.
In practice that means one named person may raise the hard floor, with product and support agreeing, because they own the users it affects and the tickets it creates. It also means the decision is reviewed: the book's review rubric lists a release or minimum-version strategy affecting old clients among the conditions that warrant a formal architecture review, and its release budgets include the minimum-version affected population, so the number of people a block touches is known before anyone presses it.
The book's deprecation record is a good template for the decision itself. It states what is being retired, the affected clients and operations, the replacement path, observed traffic and adoption, deadline and risk, the rollback or extension decision, and the final deletion evidence. If you cannot fill in the observed traffic line, you are not ready to fill in the deadline.
If the owner is a person above you and the tail is not moving, raising it is an escalation, not a complaint. How to do that well is in how to escalate as a tech lead.
A one-page version support policy
This fits on one page, and every field is something a reviewer can check. Fill in your own values; the ones that matter most are the measure, the floors and the names.
| Field | What to write |
|---|---|
| Measure | The journeys counted per version per day (for example checkout attempts and logins), by platform and region |
| Sunset rule | Share below a floor you chose for a number of consecutive days, or a maximum age, whichever comes first |
| Stall rule | If a tail stops shrinking for a chosen period, the owner takes it to product as a decision |
| Recommended version | Who moves it, how often, and what the prompt says |
| Minimum supported version | Who may raise it, with whose agreement, on which evidence, with how much notice |
| Justified reasons for a block | Compromised credentials, unsafe policy, legal requirement, unmaintainable server contract |
| Never | Block mid-journey; block on missing policy data; raise the floor to a version still in rollout; raise it to delete code |
| Server rule | Unsafe operations are refused by the server for every version, with a stable reason |
| Contract rule | Additive changes only while a version is supported; removal follows expand and contract |
| Queued work | Payload versions accepted from the oldest supported client, and the recovery path for older ones |
| Review | Floor changes go through architecture review; the affected population is stated before the change |
A policy like this also ends the circular meeting where everyone agrees old versions should keep working and nobody says until when.
What I listen for when a team describes its support window
| Topic | Mid-level | Senior | Staff |
|---|---|---|---|
| Window | "We support the last few versions." | A floor based on usage data. | A floor and a duration per journey, by platform and region, with a maximum age and a stall rule. |
| Forced update | "We force an update when we need to." | Soft prompt first, hard block for breaking changes. | A graduated ladder; the hard floor only for listed risks, never mid-journey, never to a version still in rollout. |
| Backend | "Old versions get an error." | Old endpoints kept until usage drops. | Additive contracts, expand and contract, and server-side refusal of unsafe operations for every version. |
| Ownership | "The team decides." | The tech lead decides. | A named owner with product and support agreeing, a deprecation record and a review. |
The staff column is the same toolset with the users in it. The same instinct decides the mobile architecture migration interview question, where the old path cannot be deleted until the fleet has moved.
Questions engineers ask about supporting old app versions
How long should you support old versions of a mobile app?
There is no industry number that fits every app. Measure what share of a critical journey each version still carries, set a floor and a duration (for example, below a chosen share for a chosen number of consecutive days), add a maximum age as a backstop, and give one named person the right to end support. The window is the answer that falls out of that policy for your own fleet.
What is the difference between a soft update and a forced update?
A soft update recommends a newer version and lets the user keep going. A forced update, or hard minimum version, blocks the app until the user updates. Between the two sit degradation with an explanation and a deadline with reminders. The hard block is the last step, reserved for narrowly justified risk.
When should you force users to update a mobile app?
When an old version is unsafe to keep serving: compromised credentials, an unsafe policy, a legal requirement, or a server contract that can no longer be maintained. Wanting to delete old code is not on that list. Raise the floor only to a version that has already completed its rollout, and never block a user in the middle of a journey.
Does a minimum version check protect the backend?
No. A version check runs in the client, and the client is not an authorization boundary. Anything an old version must not do has to be refused by the server, for every version, with a stable reason the client can show. The minimum version check is the polite front end of that refusal.
