Skip to content
Practitioner10 min readUpdated September 2026

Feature Flags And Staged Rollout On Mobile

On mobile you cannot roll back, so flags are not a convenience — they are the safety mechanism that replaces rollback. Remote config architecture, kill switches, default-off discipline, flag debt, and rollout halts that fire without a human.

On the web, feature flags are a convenience. They let you decouple deploy from release, run experiments and reveal features on a schedule. Pleasant, useful, optional. If a deploy goes wrong you roll it back, and the flag was a nice-to-have.

On mobile, there is no rollback. Once a binary is in users' hands, it is in users' hands. You can stop distributing it, which does nothing for anyone who already installed it. You can submit a fix, which goes into a review queue of uncertain length, and even after approval you depend on users updating — a process that takes days for the fast majority and, for a meaningful tail of your installed base, effectively never.

This changes what flags are. They stop being a product-management convenience and become the primary mechanism by which you retain any control over code that has left the building. A flag in a mobile app is the only lever between a bad change and every user who has it.

The discipline is different too. Web flag hygiene can be sloppy because the blast radius of a mistake is small and reversible. Mobile flag hygiene cannot, because a flag that does not work — evaluated before config loaded, with an off-path nobody ever tested, or removed from the server while old clients still read it — is a safety mechanism that fails exactly when you reach for it.

The two systems you are actually building

People say "feature flags" and mean one thing. On mobile there are two, with different requirements, and conflating them causes most of the trouble.

Remote configuration is a set of values the client fetches and caches locally. It governs behaviour: which variant of a screen to show, which endpoint to call, whether a feature is reachable. It must work offline, so every value needs a compiled-in default, and it must be cheap to read, so the client reads the cache rather than the network at the point of decision.

Targeting and rollout is the server-side logic deciding which clients get which values: percentages, cohorts, regions, build ranges, internal testers, experiment arms. It lives on the server precisely because you want to change it without shipping.

The architecture that follows is unglamorous and worth stating plainly.

Fetch on a schedule, apply on a boundary. Applying config mid-session produces an app that changes behaviour while someone is using it. Fetch in the background, cache the result, and activate new values at a session boundary. This makes flag state stable within a session, which is what makes bug reports interpretable.

Compile a default for every flag. The default is the behaviour on first launch, on a cold install with a failed fetch, in an airport with no signal, and on the day your config service has an incident. It must be the safe behaviour, which for new features means off.

Evaluate flags in one place. A flag read scattered through fifty call sites cannot be audited, logged or removed cleanly. Route reads through a single accessor that also records which flags were evaluated.

Version the flag schema, not just the values. Old clients read your config forever. Removing a key because the feature is now permanent will, on a client that still reads it, fall back to the compiled default — which may be the old behaviour. Treat the config contract as seriously as an API contract, and assume every version you shipped is still out there.

Default-off is a discipline, not a setting

The rule sounds trivial: new flags default to off in the shipped binary. It is violated constantly, and the violations are subtle.

A flag defaults to off, but the code path around it was written assuming it would be on, and the off-path was never exercised after the third week of development. When you flip the flag back off in anger, you discover the off-path reads a field that is now null, or that somebody tidying up deleted it six weeks ago.

The off-path is a safety mechanism. Safety mechanisms that are never tested are decorations.

Test both sides in CI. For any flag gating a user-visible path, the automated suite should run the critical journeys with the flag on and with it off. This doubles a subset of your tests, which is the price of the guarantee. Do it for every flag you would actually reach for in an incident, not for every flag.

Never let the off-path become a migration. If turning a feature off requires reversing a data migration, or the new code wrote data the old code cannot read, the flag is not a kill switch. It is a one-way door with a switch painted on it. Make data changes backwards-compatible before you design the flag.

Default-off applies to the server too. A config system where new keys default to on for the whole population is a footgun regardless of what the client defaults say.

Kill switches are a different thing from feature flags

The requirements differ, and teams routinely build one while assuming they have the other. A feature flag answers: should this user see this feature? A kill switch answers: should this capability be doing anything at all, right now, for everyone?

PropertyFeature flagKill switch
Question it answersWhich experience does this user getShould this be running at all
Typical scopeA cohort, a percentage, a regionEveryone, immediately
Who flips itProduct, during a launchOn-call, during an incident
Expected latencyNext session is fineAs fast as the platform allows
DefaultOff for new featuresOn — it is guarding existing behaviour
LifetimeRemoved when the feature is permanentPermanent, for anything load-bearing

Things that deserve a permanent kill switch: any client behaviour generating significant backend load, any background sync or upload, any third-party SDK that could hang, any polling loop, any aggressive retry, any prefetch. On a bad day each of these turns your mobile fleet into a distributed denial-of-service attack against your own infrastructure, and the fleet does not stop when you ask it nicely.

The uncomfortable property of a kill switch is that it depends on the client fetching config, which depends on the very backend that may be struggling. The config endpoint should therefore be trivially cheap, served from a cache or edge, and independent of the services it might need to kill. A kill switch behind an overloaded API gateway is not a kill switch.

Staged rollout with criteria that fire without you

Platform stores generally provide a mechanism for releasing a new version to a fraction of the user base and increasing it over time, with the ability to halt. The specifics — granularity, how halting behaves — differ by platform and change over time, so check current vendor documentation rather than relying on how it worked when you last set it up. The pattern is stable.

The value of staged rollout is not that it is gradual. It is that it creates a window in which a bad build has reached few enough people that stopping is cheap. That window is only useful if something is watching it.

Define the halt criteria before the rollout starts. Writing criteria while watching a graph move is how you talk yourself out of halting. They belong in the release checklist alongside the version number.

Tie them to comparative, not absolute, thresholds. An absolute crash-free rate target is hard to set and easy to argue with. A relative one — this version compared to the version it is replacing, over the same window, on a comparable population — is far more robust and harder to dismiss. Regression against the previous version is the signal you care about.

Cover more than crashes. A build that does not crash but fails to load content, breaks sign-in on one operating system version, or doubles cold start time is a bad build. Include error rates on critical flows, completion rates for key journeys, responsiveness signals, and at least one business metric that would notice a broken funnel.

Automate the halt, or at least the alarm. A criterion that requires someone to notice works during office hours on weekdays. Wire the signals into your on-call alerting, and where the platform supports programmatic control, make the halt automatic. A false halt costs a day. A missed halt costs a release cycle and a support queue.

Stage the flag separately from the binary. The binary rolls out to a percentage of devices; the flag reveals a feature to a percentage of users. Shipping a binary with a new feature already on for everyone conflates them and makes attributing any regression much harder. Ship the binary quietly, get it to a healthy share of the base, then start the feature rollout as a separate act with its own criteria.

Flag debt is real debt

Every flag is a branch in the code, a row in a config, a dimension in the test matrix and a thing a new engineer has to understand. Flags are cheap to add and expensive to keep, which is the classic shape of an accumulating liability — technical debt in the economic sense: sometimes worth taking on deliberately, always worth tracking.

The test matrix is the part people underestimate. Independent boolean flags produce combinations that multiply; ten of them describe more than a thousand possible configurations of your application. You will not test them all and should not try, but be honest that you are testing a tiny sample of the space your users are distributed across, and that some untested combinations are live right now.

Mitigations, in order of value:

Give every flag a type and an expiry at creation. A release flag decouples deploy from reveal and goes shortly after full rollout. An experiment flag lasts for the experiment. An operational flag or kill switch is permanent by design. A permission or entitlement flag is not a flag at all — it is business logic, and belongs in the domain model. Only the third category should outlive a quarter.

Fail the build on expired flags. A check that reads flag metadata and errors when a release flag is past its expiry is a few hours of work and the only mechanism that reliably works. Reminders do not work. Tickets do not work. A red build works.

Reduce independence. Where two flags are never meaningfully combined, model them as one enumerated setting with named states rather than two booleans. The combinatorial explosion comes from independence, and much of that independence is accidental.

Count them, and remove the flag and the dead path in the same change. Live flag count and the age of the oldest belong on the same dashboard as your other health metrics; a count that only rises is a leading indicator of a codebase becoming hard to reason about. Deleting the config key while leaving both code paths in place is cleanup theatre.

What to do on Monday

Inventory your flags. Every key in the config system, when it was created, what it gates, and whether anyone knows. Sort by age. The bottom of that list is a conversation about what your codebase actually contains.

Pick the client behaviour that would do the most damage to your backend if it misbehaved — the sync loop, the upload queue, the retry policy — and check whether you can turn it off from the server today, without shipping. If you cannot, that is the next thing to build, ahead of whatever is currently next.

Then write halt criteria for your next release before you submit it: three or four comparative thresholds, an observation window per rollout step, and a rota that gets alerted. Put it in the release checklist so it is a property of every release rather than a thing you did once.