Feature Flags with Firebase Remote Config – A Step-by-Step Guide
A feature flag is only as good as its fallback. Firebase Remote Config makes it easy to add a flag — define a parameter, publish a value, read it in the app — but the two details that decide whether your kill switch actually works under pressure are the ones the quickstart skips: what value the app uses before it has ever fetched anything, and how long a client can go without seeing your latest change because of throttling.
This guide builds one concrete flag: a new_checkout_flow boolean gating a rewritten checkout screen in a mobile app, rolled out to 10% of users first. You'll wire up default values, publish a percentage-rollout condition, trace one app launch through the fetch-and-activate cycle, and then deliberately break the rollback path to see why a missing default value turns a kill switch into a crash. For the self-hosted alternative to this pattern, see feature flags with LaunchDarkly and Unleash; for the broader class of keeping a system running while something behind it is failing, see graceful degradation patterns.
What Remote Config actually decouples
The point of a feature flag is decoupling deploy from release: you ship the new checkout screen's code in this week's app build, but it stays behind a flag that defaults to off until you're ready — and turning it on (or back off) doesn't require another app store review cycle. Remote Config is Firebase's mechanism for that: parameters defined in the console, fetched by the client SDK, and evaluated locally against conditions (user property, percentage bucket, app version) you configure without shipping new code.
The trade this makes explicit: the app must ship with a safe default value baked in for every flag, because a fresh install or a client that has never successfully fetched has nothing else to read. Treat the bundled default as the real production behavior for some fraction of your users at all times, not a placeholder.
Quick reference
- Default values ship inside the app binary — changing a default requires a new app release, not a console update.
- Remote Config values are strings/booleans/numbers evaluated client-side; there is no server round-trip per screen render once fetched.
- A parameter with no condition set behaves identically for all users — conditions (percentage, user property) are what create a gradual rollout.
- Remote Config is not a database — do not use it for values that need strong consistency or that change per individual transaction.
Remember this
The bundled default value is not a placeholder — it's the actual behavior served to every user who hasn't fetched yet, so it must be the safe, known-good state, not a stub.
Wiring the flag: defaults, fetch, activate
The client-side contract has three parts every integration needs: set in-app defaults before any UI decision reads the flag, call fetchAndActivate to pull and apply the latest published values, and read the flag through the SDK's typed getter rather than parsing a raw string yourself. Skipping the defaults step means the first read of an un-fetched flag returns Remote Config's own fallback (empty string / false), not your app's intended safe behavior, which is a subtly different bug from what most teams expect.
Quick reference
- Set
defaultConfigbefore the first read on every code path, including error/offline paths — not just the happy path. minimumFetchIntervalMillisthrottles how often the SDK re-fetches from the network — set it low in development, hours in production to control quota use.fetchAndActivatereturns a boolean indicating whether new values were activated; afalsereturn with defaults set is still safe, just not fresh.- Read flags through typed getters (
getBoolean,getNumber,getString) — never parsegetValue().asString()manually for a boolean.
Remember this
fetchAndActivate failing is not the same as the flag being wrong — it only stays safe if defaultConfig was set to the value you'd choose if the network never answered.
Percentage rollout and the fetch-throttling trap
Publishing a 10% rollout condition in the console (a percentage bucket keyed by a stable per-install randomization) changes what value is served for matching users — but it does not push that change to devices instantly. Clients only see the update on their next fetchAndActivate call, gated by minimumFetchIntervalMillis. In production, that's commonly set to an hour or more to control fetch quota, which means a console change can take up to that long to reach an already-running app.
This is the trap that catches teams during an incident: they flip the flag off in the console expecting an immediate kill switch, then watch reports keep coming in from users whose app hasn't fetched again yet. The fix is not fetching more often for everyone (that burns quota and battery) — it's designing the kill-switch path to also trigger on app foreground/resume, so an already-open app checks again sooner than its normal throttle window.
Quick reference
- The percentage bucket is computed from a stable per-installation identifier, so a user stays in or out of the rollout across sessions rather than re-randomizing on every fetch.
- Force a fetch on
applicationDidBecomeActive/ app-resume for kill-switch-critical flags — this is the realistic path to a faster-than-throttle-interval update during an incident. - Firebase enforces fetch quotas per project; a rollout condition change is not a substitute for testing your throttle interval's real-world propagation time before an incident happens.
- Log the timestamp of each successful
fetchAndActivateper client during a rollout so you can measure actual propagation delay, not just assume it matches your configured interval.
Remember this
A console flag flip is only a kill switch for the fraction of clients that have fetched since you flipped it — the propagation delay is bounded by your fetch interval, not by when you clicked publish.
The missing-default failure and the decision rule
The realistic incident to design against: a team ships new_checkout_flow with no defaultConfig entry, relies entirely on the console default, and later that week a user opens the app fully offline on first install. The trigger is offline-first-launch; the symptom is a checkout screen that renders neither the old nor new flow correctly because the code branches on a value that is neither true nor a safe false, it's Remote Config's library-level fallback which the app never explicitly chose. Root cause: treating the console's published default as the only default that matters, when the bundled defaultConfig is what actually governs an unfetched client. Recovery is adding the explicit default and shipping it in the next release — which, notably, takes an app store cycle, unlike a console change.
The decision rule: use Remote Config for flags that need gradual percentage rollout, targeting conditions, and instant server-side toggling without a client update — most product feature gates. Reach for a heavier, self-hosted system like Unleash when you need audit logs, RBAC across teams, or flag evaluation for backend services rather than mobile/web clients, where Remote Config's client-fetch model is a weaker fit.
Quick reference
- Always set
defaultConfigfor every parameter you define in the console — treat a parameter with no client-side default as an incomplete flag, not an optional step. - Test the true offline-first-launch path explicitly (airplane mode, fresh install) before shipping any flag your rollback plan depends on.
- Reserve Remote Config for client-facing rollout/targeting; use a server-side flag system for backend service behavior where a mobile fetch-and-activate cycle doesn't apply.
- Pair every kill-switch flag with a foreground-refetch trigger so an open app can react faster than the default throttle window during an incident.
Remember this
A flag with no explicit default isn't undefined behavior by accident — it's Remote Config's own fallback silently standing in for a decision your team never actually made.
Key takeaway
Wire up new_checkout_flow: a bundled defaultConfig of false, a console parameter with a 10% percentage-rollout condition, and a foreground-refetch trigger. Verify success by force-fetching on a test device inside the 10% cohort and confirming the new checkout screen renders, while a device outside the cohort still sees the old flow.
Then break it deliberately: remove the defaultConfig entry, force a fresh install in airplane mode, and confirm what actually renders — expect an inconsistent state, not a clean fallback. Recover by restoring the explicit default and re-testing the same offline-first-launch path. Pass criterion: the offline-first-launch renders the old (safe) checkout flow every time, with no code path relying on Remote Config's own library-level fallback.
Related Articles
Explore this topic