Feature flags are a double-edged sword. They let you ship code safely, experiment with users, and roll back instantly—but only if they’re cleaned up afterward. Most teams discover too late that stale flags accumulate like rust in a codebase, quietly inflating complexity, degrading performance, and turning routine maintenance into a guessing game.
The surprise isn’t that flags linger. It’s that engineers rarely know how many are still active or what risks they pose. What started as a quick toggle for a feature branch can become a hidden maintenance burden, with engineers spending up to 42% of their time on technical debt that could have been avoided. The good news: you can surface the problem in under a minute—without dashboards, APIs, or extra accounts.
Why Stale Flags Are a Silent Liability
Industry best practices from platforms like LaunchDarkly recommend removing most feature flags within days or weeks of deployment. Yet in practice, many flags outlive their purpose by months or even years. The cycle starts innocently: a developer adds a flag to test a new checkout flow, ships it, and moves on. Six months later, a teammate discovers an obscure conditional in checkout.ts and asks, Is it safe to delete this? The answer is usually a shrug.
That uncertainty has real costs. Stale flags increase cognitive load, as developers must trace conditional logic to determine if it’s still relevant. Some flags can even degrade runtime performance or, in rare cases, expose unintended features or data. Worse still, they contribute to the broader technical debt that eats into sprint velocity—time that could be spent building new features instead of untangling old toggles.
What Flag Debt Looks Like in Real Code
Take a typical Node.js payment service. It uses LaunchDarkly for feature management, with calls scattered across multiple files. At first glance, each line looks similar:
// checkout.ts
export async function isCheckoutV2Enabled(user: User): Promise<boolean> {
const ctx = {
targetingKey: user.id,
email: user.email,
plan: user.plan
};
return ldClient.boolVariation("checkout-v2", ctx, false);
}
// discounts.ts
const flagKey = `discount-${experimentName}`;
const enabled = await ldClient.boolVariation(flagKey, ctx, false);
// analytics.ts
const state = await ldClient.allFlagsState(ctx);But these three patterns hide vastly different levels of risk. The first is straightforward—a static flag key with a known type. The second is trickier: the flag key is built dynamically, so the system can’t determine which flag is being evaluated at runtime. The third is a migration blocker: allFlagsState isn’t compatible with modern feature flag standards like OpenFeature, meaning any change requires an architectural shift before you even start refactoring.
Most teams treat all three the same. They shouldn’t.
Run a 30-Second Audit to Reveal Hidden Risks
FlagLint, a lightweight static analysis tool, can surface your flag debt without leaving your terminal. With a single command, it scans your codebase, classifies every flag call by risk level, and delivers a report that tells you exactly what you’re dealing with—before you make a single change.
npx flaglint@latest audit ./srcWhen run against the example service above, the tool returns:
✓ Audit complete: 13 flags — 3 high risk, 10 medium risk, 0 low risk
A detailed breakdown follows, categorizing each flag and explaining the risks:
- High risk (manual review required):
- Dynamic keys (flag keys built from variables or template literals)
- Detail evaluations (calls that return metadata with no OpenFeature equivalent)
- Bulk calls like
allFlagsState(requires architectural decisions before migration)
- Medium risk (safe to automate, but still tied to vendor-specific logic)
- Low risk (already migrated or flag-free)
Export Reports for Team Visibility
The audit doesn’t modify your code—it just reveals the problem. You can export the findings in multiple formats, each suited for different workflows:
- Markdown: Ideal for pull requests and internal documentation.
npx flaglint audit ./src --format markdown- JSON: Perfect for CI pipelines, dashboards, or automated tracking.
npx flaglint audit ./src --format json --output audit.json- HTML: A self-contained, shareable report you can drop into PRs, Jira tickets, or emails—no server needed.
npx flaglint audit ./src --format html --output flag-debt.htmlThe HTML report is especially valuable for engineering reviews. It’s concise, visually structured, and highlights exactly which flags need attention and why—making it easy to prioritize cleanup across teams.
From Insight to Action: Migrate with Confidence
Once you’ve identified the debt, FlagLint provides two commands to help you act—safely and incrementally.
Preview migrations without risk:
npx flaglint migrate ./src --dry-runThis shows before/after diffs for every medium-risk flag FlagLint can safely migrate. Nothing is written to disk, so you can review changes before committing.
For example, a LaunchDarkly flag like:
return ldClient.boolVariation("checkout-v2", ctx, false);becomes:
return openFeatureClient.getBooleanValue("checkout-v2", false, ctx);Notice the argument order changes: LaunchDarkly uses (flagKey, context, fallback), while OpenFeature uses (flagKey, fallback, context). A manual find-and-replace could silently flip these, introducing bugs in production. FlagLint avoids this by using AST analysis—not text matching—to ensure accuracy.
Apply migrations safely:
npx flaglint migrate ./src --applyThis rewrites only the calls proven safe to migrate, skipping high-risk ones and reporting them for manual review. The command checks for a clean Git status before running and is idempotent—running it twice produces the same result.
Lock the boundary with CI enforcement:
npx flaglint validate ./src --no-direct-launchdarklyAdd this to your GitHub Actions workflow to prevent regressions. It exits with an error if any direct LaunchDarkly evaluation calls remain, ensuring your migration can’t silently reverse.
The Vendor-Neutral Approach to Flag Management
Many feature flag platforms offer tools that connect to their dashboards to show flag health. These can be useful, but they only see flags registered in their system—ignoring flags in other platforms or even custom implementations.
Worse, they’re designed to keep you within their ecosystem. No vendor builds an easy exit ramp for migrating away from their tool.
FlagLint takes a different approach: it only examines your source code. It doesn’t need to know what’s in your LaunchDarkly dashboard, Statsig account, or internal flag store. The question it answers is simple: What flags are actually being used in the codebase today?
That clarity is the first step toward reducing technical debt, improving maintainability, and reclaiming engineering time—without being locked into any single platform.
AI summary
Unused feature flags increase technical debt and slow down Node.js projects. Learn how to detect and remove them in under 30 seconds using FlagLint.