Feature flag questions, answered
Short answers to the things that go wrong with feature flags in practice. Each one names the causes in the order they actually occur, and points at the doc that proves it.
Can I use one SDK key for every environment?
No, and the key is not a credential you could reuse even if you wanted to. It is the selector: whichever environment issued the key is the configuration your process receives. Point staging at the production key and staging reads production flag states, which means a rollout you start in staging is a rollout in production.
Do I need a user ID to evaluate a feature flag?
Not for on/off flags or targeting rules that match on other attributes. You need one for percentage rollouts, because the bucket comes from hashing the identifier with the flag key. Without it a server SDK evaluating locally serves the first (control) variation, while the hosted evaluation endpoint spreads anonymous traffic by weight with no per-user stickiness.
Do users lose a feature if I lower the rollout?
Yes. Buckets are fixed per user, so lowering a rollout from 50% to 20% keeps everyone in buckets 0 through 19 and removes everyone from 20 through 49. It is deterministic, not a reshuffle, which is why raising the percentage again restores exactly the same people. Users who lose access see the feature disappear mid-session unless you evaluate once per session and cache the result.
Does changing a targeting rule re-bucket users?
No. A user bucket comes from a salt stored on the flag and the user key, so editing rules, adjusting weights or adding conditions leaves every assignment untouched. Two things do reassign everybody: changing the attribute a rollout buckets by, and deleting a flag and recreating it, because a new flag gets a new salt.
How do I test code that uses feature flags?
Use the test client each SDK provides. You construct it with a map of flag keys to values and it evaluates against that map, with no network calls and no initialization step. It is not registered in the normal client cache, so every call returns an independent instance and tests cannot leak state into each other.
What if I archive a flag still used in my code?
Archiving removes the flag from the configuration SDKs receive, so every remaining call site immediately falls back to the default value passed at that call site. Nothing throws and nothing logs an error, because the SDK cannot tell an archived flag from one that never existed. Remove the code first, then archive.
Why can't my browser SDK see a feature flag?
Because the flag is not marked client-side visible. Client SDK keys are public by design, so they only ever receive flags you have explicitly opted in. A server key sees every flag in the project, which is why the same key works from your backend and fails in the browser. The client SDK reports the flag as not found rather than raising.
Why did my flag change take minutes to reach my app?
Because the streaming connection is not established, so the client picks the change up on its next poll instead. Streaming is on by default and delivers changes within seconds, so a delay of roughly thirty seconds points at the poll interval taking over. Proxies that buffer responses and HTTP clients with a short timeout are the usual reasons the stream drops.
Why did my targeting rule not match?
Usually one of three things: an earlier rule matched first and stopped evaluation, the attribute your rule tests was never included in the evaluation context, or the value is present but a different type or case than the rule expects. Rules run top to bottom and the first match wins, so rule order is part of the logic.
Why do teammates get different variations?
Because a rollout buckets by user key by default, and each user hashes independently. Two colleagues in the same workspace are two separate users, so a 50% rollout splits them roughly half the time. Set the rollout to bucket by an account or organization attribute instead, and everyone sharing that value moves together.
Why do two SDKs return different variations?
Almost always because the evaluation context is keyless in one of them. Bucketing needs a stable user key, and the two evaluation paths handle its absence differently on purpose: a server SDK evaluating locally serves the first variation, while the hosted endpoint assigns a fresh random bucket per call. Pass the same user key everywhere and the two agree, because every SDK runs the same hash.
Why do two users get different variations?
In a percentage rollout that is the expected behaviour. Each user is hashed together with the flag key to produce a bucket from 0 to 100, so a 30% rollout puts roughly three users in ten on the new variation. The same user always lands in the same bucket for the same flag. Different flags hash independently, so being in the 30% for one says nothing about another.
Why does my flag flash the wrong value on load?
Because the browser SDK returns the default value you passed until its first flag payload has arrived, and your UI renders during that window. The reason string is FlagNotFound rather than an error, since the flag is simply absent from an empty snapshot. Wait for initialization before rendering the gated region, or render a neutral state until the client is ready.
Why does my flag work in dev but not production?
Because every environment holds its own configuration, and enabling a flag in development changes nothing in production. Targeting rules and rollout percentages are per environment as well, so a flag can be on in both and still serve different values. A new environment starts every existing flag in the off state deliberately, so nothing goes live by accident.
Why does my flag work locally but not in Docker?
Almost always because the SDK key did not reach the container. A key read from a local env file that is not passed through in your compose file or Dockerfile arrives empty, and the SDK serves your default value rather than failing loudly. The second cause is outbound network: a container on an internal-only network cannot reach the evaluation endpoint, so the first payload never arrives.
Why doesn't my rollout hit the percentage I set?
Because bucketing assigns each user a fixed number from 0 to 99 and counts how many land under your threshold. It does not meter traffic. With a few hundred users the observed share can sit several points either side of the configured one, and that is expected. Persistent skew usually means anonymous contexts, which the hosted endpoint spreads randomly on every call instead of bucketing.
Why doesn't my segment match any users?
Usually because the rule cannot resolve the segment at all, which fails closed and matches nobody. A client SDK that was never given segment data behaves the same way. If the segment does resolve, the next suspect is condition logic: segments use one flat condition list with a single AND or OR, so a segment built to catch two different user groups with AND matches nobody by construction.
Why doesn't my semver targeting rule match?
Because the value you are sending does not parse as a semantic version. An unparseable string matches nothing rather than falling back to text comparison, which is why the rule looks dead instead of behaving oddly. A leading v is fine and 2.0 compares equal to 2.0.0, but a build number like 1042 or a date will never satisfy a semver condition.
Why is my feature flag returning the default value?
Almost always because the SDK cannot find the flag, not because evaluation failed. The usual causes, in order: the SDK key belongs to a different environment, the flag is off in that environment, the key is misspelled, the client has not finished initializing, the requested type does not match the variation type, or the flag is not marked client-side visible and you are asking from a browser.
Why is my feature flag returning the wrong type?
Because the SDKs do not coerce between types. If the variation holds a string and you call the boolean method, you get the default you passed rather than a converted value. The dashboard shows the flag serving one thing while your code reports another, which looks like a stale cache and is actually a type mismatch.
Why is my flag evaluation returning an error?
Because the evaluator picked a variation key the flag no longer defines, which normally means a rule or fallthrough still references a deleted variation. The SDK serves the default value you passed and reports Error rather than guessing. A variation that exists but holds a null value is not this case, so the reason string is a reliable way to tell a config problem from a data one.
Looking for something longer?
The documentation covers every SDK in depth, the glossary defines the vocabulary, and the blog works through full patterns end to end.
Try it yourself
The free Solo plan covers 10 flags and 2 environments, which is enough to reproduce everything on this page.