Blog · · 11 min read

Feature flag migration: how to switch providers without a rewrite

The SDK swap is the cheap part. What runs the schedule over is translating targeting rules between two different engines, keeping percentage buckets stable, and finding every dashboard that reads your exposure events before the old stream stops.

The short answer: a feature flag migration is not an SDK swap, and teams that scope it that way run over. The SDK change is usually one file if you wrapped the vendor, or a week of mechanical edits if you did not. The three things that actually consume the schedule are translating targeting rules between two different rule engines, keeping percentage buckets stable so users do not silently jump between variants, and finding every dashboard that reads your exposure events before the old stream stops. Plan for two to six weeks for a real codebase, run both vendors in parallel for at least a week, and delete your dead flags before you migrate anything.

That is the compressed version. The detail below is the part that decides whether the estimate holds.

Why teams migrate in the first place

Almost nobody migrates because they found a nicer dashboard. In practice there are four triggers, and knowing which one you are is what tells you how much risk is acceptable.

The first is a bill that outgrew its meter. Flag vendors charge for wildly different things, and a pricing model that was fine at 50,000 users can become the third-largest line item in your engineering budget at 5 million. This is the most common trigger and the easiest to justify, because the savings are a number you can put in a document.

The second is a vendor change you did not choose. Split is the current example: Harness acquired it in June 2024 and now sells it as Feature Management and Experimentation, with no published price and a Contact Sales button where the pricing page used to be. We documented what changed in detail on our Split.io alternative page. The third is a compliance or residency requirement that arrives with an enterprise customer, which usually forces a self-hosted or single-tenant option. The fourth is consolidation, where flags fold into an observability or product analytics tool the company already pays for.

Triggers one and four leave you time. Triggers two and three usually come with a deadline attached, and a deadline is exactly the pressure that makes teams skip the parallel-run step described later, which is the step that catches the expensive mistakes.

Step one: delete before you migrate

This is the highest-leverage hour in the whole project and it happens before you write any code. Export the full flag list from your current vendor and sort every flag into three buckets.

  • Permanent flags. Kill switches, plan entitlements, per-tenant configuration. These are infrastructure and they migrate.
  • In-flight release flags. Features currently rolling out. These migrate, and they need their exact current rollout percentage preserved.
  • Dead flags. Fully on or fully off for months, with the losing branch unreachable. These do not migrate. They get deleted.

In most codebases the third bucket is somewhere between a quarter and half the list. Every flag you delete is a targeting rule you do not have to translate, a test you do not have to write and a branch of code you do not have to reason about twice. If the migration gets cancelled tomorrow, this work still leaves you better off, which makes it the safest thing to do first.

The counter-argument you will hear is that deleting flags is risky mid-migration. It is riskier to carry them. A flag that nobody has looked at in eight months is a decision nobody has validated in eight months, and porting it forward makes it somebody's problem for another year.

Step two: wrap the vendor, even if you are staying

The single design decision that determines whether a migration costs one file or one month is whether flag evaluation goes through your own function. If every service calls the vendor SDK directly, your migration touches hundreds of call sites. If they all call your isEnabled(), the change is contained.

// Your own module. The only place the vendor name appears.
import { toggled } from '@toggled/sdk';

export async function isEnabled(flag: string, user: User): Promise<boolean> {
  return toggled.isEnabled(flag, {
    userId: user.id,
    attributes: { plan: user.plan, country: user.country },
  });
}

// Everywhere else stays vendor-agnostic:
if (await isEnabled('checkout-v2', user)) { ... }

Write this wrapper even if you have no intention of switching. It is twenty lines, it gives you one place to add logging and metrics, and it means the question "what would it cost to leave?" has an answer that is not a shrug. If you want the standardized version of that idea rather than a hand-rolled one, the trade-offs are covered in our assessment of whether OpenFeature is worth adopting, with the important caveat that a shared API still does not make two vendors behave the same way.

Step three: translate targeting, never copy it

This is where migrations go wrong quietly, which is the worst way for them to go wrong.

Flag vendors do not share a rule model. One evaluates rules top to bottom and stops at the first match. Another evaluates all rules and applies a precedence order. One supports prerequisite flags where flag B only applies if flag A is on. One models audiences as reusable named segments, another inlines the conditions on each flag. Exporting rules from vendor A and importing them into vendor B produces something that looks correct in the UI and behaves differently in production.

The reliable method is boring: rewrite each rule by hand, then write a test that asserts specific users get specific answers. Pick five to ten representative users per flag, including the awkward ones (the internal staff account, the enterprise customer on a custom plan, the logged-out visitor), and assert the expected variant for each under both vendors. Those assertions are what turn "the rules look the same" into "the rules are the same."

Pay particular attention to the default. Every evaluation has a fallback value for when the flag is missing or the SDK cannot reach the service, and vendors differ in what happens on a network failure. A flag that fails open on one platform and fails closed on another is a production incident waiting for a bad afternoon.

Step four: keep the buckets stable

Percentage rollouts are implemented by hashing a user identifier and comparing the result against the rollout threshold. The hashing algorithm and the salt are vendor-specific. That means the same user, at the same 20 percent rollout, on two different platforms, will land in different groups.

For a feature at 100 percent this does not matter. For a feature mid-rollout it matters a great deal, because a subset of users who were seeing the new checkout flow yesterday will see the old one today, and vice versa. If you are running an experiment, the migration invalidates the results outright, because the population changed halfway through the measurement.

There are two honest ways to handle this. Either finish or pause in-flight rollouts before cutting over, taking each flag to 0 or 100 percent so bucketing becomes irrelevant, or accept the reshuffle and restart any experiment from a clean baseline afterwards. Pretending the buckets carry over is the third option and it produces confusing data for weeks. Our guide to running a gradual rollout covers the stepping pattern to resume with once you are on the other side.

Also normalize your user key first. If one system keys on an internal user ID and the other on an email hash, you get the reshuffle even for flags you thought were stable, plus a targeting rule that matches nobody.

Step five: run both, and log the disagreements

For at least a week, evaluate every flag against both the old and the new platform on the same request. Serve the old answer. Log the new one. Alert when they differ.

const oldValue = await legacy.isEnabled(flag, user);
const newValue = await toggled.isEnabled(flag, user);

if (oldValue !== newValue) {
  logger.warn('flag_mismatch', { flag, userId: user.id, oldValue, newValue });
}

return oldValue;   // old platform still authoritative during the shadow week

This costs one extra evaluation per request and it is the cheapest insurance in the project. Every rule translation error from step three and every bucketing surprise from step four shows up here as a log line rather than as a support ticket. A clean mismatch log for seven days across real traffic is the only evidence that justifies the cutover, and it is far more convincing to a nervous stakeholder than a test suite.

Cut over one service at a time, starting with the least critical one. Keep the old vendor's contract alive for a billing cycle past the cutover. The cost of one extra month is trivial next to the cost of discovering a missed dependency with no way back.

Step six: follow the exposure events downstream

The part almost every migration plan forgets. Flag evaluations usually emit exposure events, and those events rarely stop at the vendor dashboard. They land in a warehouse, feed product analytics, populate experiment readouts, and occasionally drive real logic such as usage-based billing or entitlement checks in a reporting job.

Changing vendors changes the shape and the name of those events. Before the old stream stops, inventory every consumer: scheduled queries, BI dashboards, models built on the exposure tables, and any alerting that watches them. On anything larger than a handful of tables it is worth using tooling that can trace which reports and downstream tables actually depend on a given source, because the ones that break are never the dashboards people look at daily. They are the quarterly reports nobody opens until the quarter ends.

Then confirm the retention story. If your experiment history lives only in the old vendor's UI, exporting it is a migration task with a deadline attached to your contract end date, not something to handle later.

How long it actually takes

SituationRealistic elapsed timeWhat dominates
One service, wrapper already in place, under 30 flagsDaysThe shadow week, not the code
Several services, one language, 50 to 200 flags2 to 4 weeksRule translation and testing
Polyglot, direct SDK calls everywhere, 200+ flags1 to 3 monthsCall site edits, then rule translation
Any of the above, plus live experimentsAdd 2 to 4 weeksWaiting for experiments to conclude

The variable that moves these numbers most is not team size or flag count. It is whether the flags were disciplined in the first place: named consistently, owned by someone, and expired on a schedule. A codebase that followed reasonable feature flag best practices migrates in a fraction of the time of one where flags accumulated for three years, because most of the work is understanding what each flag does, and that understanding is either written down or it is not.

The mistakes worth naming

Migrating everything at once. A big-bang cutover across every service on a Friday removes your ability to compare behavior and concentrates all the risk in one window. Service by service takes longer on the calendar and less time overall.

Cancelling the old contract early. The savings look good on a spreadsheet and remove your rollback. Overlap for one billing cycle.

Treating permanent flags as release flags. Kill switches and entitlement gates need the same care as production configuration, because that is what they are. Verify each one manually rather than trusting a bulk import. If a feature flag kill switch is wired to the wrong default after the move, you find out during the incident it was supposed to prevent.

Skipping the shadow week because the tests pass. Tests assert what you thought to assert. Real traffic asserts everything else.

Choosing what to migrate to

One last thing worth saying, given how much work the above represents: pick a destination you will not have to leave again in eighteen months. The most common reason for a second migration is the first one solved a feature gap and ignored the meter. A platform priced on something that grows with your success, unique users, requests or events, will eventually reproduce the bill that started this. Flat pricing with unlimited seats and flags does not have that failure mode.

If you are still comparing options, our best feature flag tools comparison lists what each vendor charges with verified July 2026 numbers, and the two most common displacement cases have their own pages: LaunchDarkly alternative and Split.io alternative.

Early access · launching soon

Ship your next release behind a flag

Join the early-access list and be first in line when spots open. Flat pricing, unlimited seats, no card required.

No card required · your data stays yours