Feature flags are fetched and evaluated in the browser after the application boots. Variant content flickers on first render, flag resolution adds about 240 ms at the 95th percentile before the first paint, and a flag that crashes the app during start-up cannot be turned off for the users it crashed. The product ships weekly and users already in an experiment must not be re-bucketed. Give the sequence to move evaluation server-side, where the two evaluators can diverge, the point of no return, and how long it really takes.
Show the full answer Hide the answer
The sequence
- Freeze the bucketing function and extract it as one library with a test vector. Ten thousand identities x every flag key, with the expected variant in a checked-in file. Both evaluators must reproduce it byte for byte. Nothing has shipped, so this step is free to redo.
- Evaluate on the server in shadow mode. The HTML or edge response carries an evaluated flag payload the client logs and ignores, while the client keeps deciding. Compare per impression and drive the mismatch rate below roughly 1 in 100,000 before going further. This is the step that earns the migration - the only place divergence is cheap.
- Flip reads for one low-risk flag behind a meta-flag: use the injected payload when present, fall back to the client's fetch when absent. Reversible per flag.
- Flip the rest in batches ordered by blast radius, keeping the client fetch as fallback for two release cycles.
- Delete the client evaluator and the boot-time fetch, keeping a last-known payload in local storage for offline starts.
Where state can diverge
- Attribute availability. The server cannot see viewport, device memory or reduced-motion; the client cannot see entitlements without a request. A rule reading an attribute one side lacks evaluates differently and says nothing about it.
- Identity. If the anonymous id is set by client JavaScript and the server needs it before the first byte, first-time visitors have no id at render time, and any assignment you invent re-buckets them. Moving the id to a server-set cookie fixes it and is itself one-way.
- Time. A flag changed between server render and client boot - harmless for a cosmetic flag, not for a holdout.
- Hashing. The same algorithm over UTF-8 bytes versus a JavaScript string, or a different salt order, produces different buckets.
The point of no return
Deleting the client evaluator. After that, an unreachable flag service during server render means no flags at all, so the cached payload and a compiled-in default per flag must be in place before step 5.
The second irreversible step is moving identity to a server-set cookie: you cannot go back without re-bucketing live experiments, which invalidates whatever is running.
The rollback at each stage
Steps 1 and 2 roll back by reverting a deploy. Step 3 and 4 roll back per flag by clearing the meta-flag - the client fallback is still there. Step 5 has no rollback; it has a prerequisite.
The failure to plan for is step 2 running in production with a mismatch rate stuck near 1 in 2,000. That is not noise, and shipping anyway breaks live experiment assignment silently - no error, no alert, just a treatment group that no longer means anything.
What it costs
Two evaluators in the codebase for a quarter, a shadow-logging path on every response, and 1-2 KB of injected JSON in the HTML. In exchange the flicker and the 240 ms pre-render delay disappear, and a flag becomes usable as a safety control rather than a release convenience.
How long it really takes
With 40 flags and weekly releases, 8 to 12 weeks, of which 3 to 4 are step 2 accumulating impressions on low-traffic flags. Step 5 then slips a quarter because two flags read client-only attributes and nobody wants the exception. Budget for that rather than discovering it.
When not to run this migration
Anything depending on the client's own capabilities - viewport class, reduced-motion, device memory, offline state - cannot be evaluated anywhere else. The decision rule is clean: evaluate on the server anything that affects the first render or is used as a safety control; evaluate on the client anything whose inputs only the client has. A product with no variant-dependent first render and no flag used as a kill switch should not run this migration.