Skip to content

Feature Flags with Firebase Remote Config – A Step-by-Step Guide

Core Concept LearningAugust 3, 20267 min read

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.

Where Remote Config sits between console and app
Where Remote Config sits between console and app

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.

One app launch fetching a rollout flag
One app launch fetching a rollout flag

Quick reference

  • Set defaultConfig before the first read on every code path, including error/offline paths — not just the happy path.
  • minimumFetchIntervalMillis throttles how often the SDK re-fetches from the network — set it low in development, hours in production to control quota use.
  • fetchAndActivate returns a boolean indicating whether new values were activated; a false return with defaults set is still safe, just not fresh.
  • Read flags through typed getters (getBoolean, getNumber, getString) — never parse getValue().asString() manually for a boolean.
Reading a flag with no defaults set
1import { getRemoteConfig, fetchAndActivate, getBoolean } from "firebase/remote-config";2 3const rc = getRemoteConfig(app);4 5async function shouldShowNewCheckout() {6  await fetchAndActivate(rc); // no defaults set; app hasn't confirmed it succeeded7  return getBoolean(rc, "new_checkout_flow");8  // On a slow/offline fetch, this silently returns false — indistinguishable9  // from an intentional "not in rollout" decision.10}
With defaults and explicit fetch-result handling
1const rc = getRemoteConfig(app);2rc.defaultConfig = { new_checkout_flow: false }; // known-safe, shipped in the binary3rc.settings.minimumFetchIntervalMillis = 3600_000; // throttle: 1 hour in production4 5async function shouldShowNewCheckout() {6  try {7    const activated = await fetchAndActivate(rc);8    // activated === false means cached/default values were used, not fresh ones —9    // still safe, because defaultConfig is known-good.10  } catch {11    // Expected: offline fetch failure falls back to defaultConfig, not a crash.12  }13  return getBoolean(rc, "new_checkout_flow");14}15// Break it: remove rc.defaultConfig and force fetchAndActivate to reject —16// expected: getBoolean now returns the library's built-in false, which may17// not match your intended "safe" state for every flag.

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 fetchAndActivate per 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.

Kill switch: rolling back a broken feature without a redeploy
Kill switch: rolling back a broken feature without a redeploy

Quick reference

  • Always set defaultConfig for 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.

Share:

Related Articles

Long-running autonomous agent sessions — such as multi-package refactoring, test suite executions, or cloud deployments

Read

Deploying new code directly to 100% of production users in a single release introduces massive risk. A single unhandled

Read

Traditional perimeter-based security ('Castle and Moat') assumes that all traffic inside a private network or Kubernetes

Read

Keep learning

Follow a structured path or browse all courses to go deeper.