Feature flags are one of those tools that start off as a savior and quietly become a liability. You add a flag to decouple deployment from release. Then another. Then another. Two years later, you have 500 flags scattered across your codebase, and no one remembers what most of them do.
This isn't hypothetical. A team I worked with had a flag called `enable_advanced_caching` that was introduced in a sprint in 2021. It was toggled on in staging, never fully rolled out, and then forgotten. In 2023, a new engineer refactored the caching layer and removed the flag entirely because they thought it was dead code. The rollout caused a 45-minute outage because the flag was still being evaluated in a critical path — but with the old default value. The flag had been dead for 14 months, but no one had bothered to clean it up.
What Is Feature Flag Debt?
Feature flag debt is the accumulation of stale, unowned, or poorly named flags that add cognitive load and increase the risk of incidents. It's similar to code debt, but worse because flags are often invisible — they don't show up in static analysis unless you specifically look for them.
Signs you have feature flag debt:
- arrow_rightYou can't answer 'What flags are currently active in production?' without running a grep.
- arrow_rightA flag has been at 50% rollout for more than 6 months with no clear owner.
- arrow_rightYour CI/CD pipeline has a 'flag audit' step that everyone ignores.
- arrow_rightYou've found a flag that references a feature that no longer exists.
Feature flag debt is silent until it screams. And when it screams, it's usually during an incident.
The Cost of Not Cleaning Up
Every redundant flag in your codebase adds a small tax on every deployment. You have to reason about two code paths instead of one. You have to test both paths. You have to maintain the flag's configuration in your feature flag system. Over hundreds of flags, this tax adds up to significant slowdowns.
There's also the risk of the 'flag bomb' — a flag that's been dead for so long that its removal requires a coordinated rollout because no one knows what it actually controls. I've seen teams spend an entire sprint just to safely remove a single flag.
Outage caused by a stale feature flag that was thought to be dead
Step 1: Audit Your Flag Inventory
You can't clean what you don't know about. Start by creating a comprehensive inventory of all feature flags in your system. This includes flags from your feature flag service, environment variables, and any hardcoded conditionals.
Use a combination of static analysis and runtime logging. Here's a quick script to find flags in a Python codebase:
grep -r 'feature_flag\|is_enabled\|get_flag' --include='*.py' . | \
awk '{print $0}' | sort | uniq -c | sort -rnThat gives you a raw count. But you also need to know which flags are actually being evaluated in production. Add structured logging to your flag evaluation calls:
import structlog
logger = structlog.get_logger()
def get_flag(flag_name: str, default: bool = False) -> bool:
value = feature_flag_service.is_enabled(flag_name)
logger.info("feature_flag_evaluated", flag=flag_name, value=value)
return valueAggregate these logs in your observability platform (Datadog, Grafana, etc.) and generate a report of flags that haven't been evaluated in the last 30 days. Those are your prime candidates for removal.
Classify Flags by Risk
Not all flags are equally dangerous. Classify them into three buckets:
- arrow_rightDead: The flag is always true or always false in all environments, and the feature it controls is fully rolled out or reverted. These are safe to remove immediately.
- arrow_rightDormant: The flag hasn't been toggled in 6+ months, but still has a conditional rollout (e.g., 50% of users). These need investigation to determine the intended state.
- arrow_rightActive: The flag is actively being used for an in-progress rollout or experiment. These are not debt, but should still have an expiration date.
Never assume a flag is dead just because it hasn't been touched. Always verify the intended behavior with the original owner or through code review.
Step 2: Remove Dead Flags Safely
Removing a flag is not just deleting a line. You need to ensure that the code path you keep is the correct one. For a flag that's fully rolled out (100% true), keep the true branch. For a flag that's reverted (100% false), keep the false branch.
Here's the process I follow:
- 1Create a new branch and remove all references to the flag in code.
- 2Update tests that depend on the flag's behavior to only test the kept path.
- 3Deploy the change behind a new flag (meta-flag) to a small percentage of users first.
- 4Monitor error rates and latency for 24 hours.
- 5If no issues, roll out to 100% and remove the meta-flag.
- 6Delete the flag from your feature flag service.
# Before:
if feature_flags.is_enabled("new_checkout"):
return new_checkout_flow(request)
else:
return old_checkout_flow(request)
# After (keeping new_checkout):
return new_checkout_flow(request)This looks straightforward, but in practice, flags often wrap multiple code paths, and the branches may have diverged significantly. In that case, you might need to refactor the code to eliminate the flag entirely.
Automated Cleanup with CI/CD
The best way to prevent flag debt is to make cleanup a non-negotiable part of your development process. Add a CI step that checks for flags exceeding their expiration date:
# .github/workflows/flag-audit.yml
name: Feature Flag Audit
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check flag expiration
run: |
# Parse flag definitions from a JSON file
# Fail if any flag has expired
python scripts/flag_audit.py --fail-on-expiredAlso require that every new flag added in a PR includes an expiration date. You can enforce this with a code owner review or a simple schema validation.
The 14-Month-Old Flag That Took Down Production
- T-14 monthsEngineer adds `enable_advanced_caching` flag, toggles it on in staging.
- T-12 monthsFeature is deemed stable, but flag is never cleaned up. It stays at 100%.
- T-2 weeksNew engineer refactors caching layer, removes the flag from code because it's 'always true'.
- T-0Deployment goes out. The flag's default value in the feature flag service is false. Old code path used. Caching layer breaks. 45-minute outage.
Lesson
A flag that is always true in code is not the same as a flag that is always true in the feature flag service. The service's default may differ. Always remove the flag from both code and the service, and test the removal.
Step 3: Build a Culture of Cleanup
No amount of automation will save you if your team treats flags as permanent fixtures. Feature flags are temporary by design. They should be used for gradual rollouts, A/B testing, or kill switches — not as permanent configuration.
Here are three cultural changes that help:
- arrow_rightMake flag cleanup part of the definition of done for any feature rollout. If you ship a feature with a flag, you must schedule its removal within 2 weeks.
- arrow_rightAssign a 'flag shepherd' for each sprint — someone responsible for auditing and removing at least one stale flag.
- arrow_rightCelebrate flag removals. Seriously. Put a 'Flag Removal of the Week' in your team standup. It makes cleanup visible and rewarding.
A feature flag that lives longer than two sprints is not a flag — it's a permanent conditional branch. And that's just debt with a different name.
When to Keep a Flag
Not all flags are debt. Kill switches for emergency rollbacks are a legitimate use case. But they should be rare and well-documented. If you have more than 5 kill switches, you're doing something wrong.
Also, flags used for long-running experiments or gradual rollouts to specific user segments are fine as long as they have a clear owner and an expiration date. Treat them like technical debt tickets: they must be tracked and resolved.
Use a naming convention that indicates the flag's purpose and owner. For example: `killswitch_payment_v2_<owner>_<date>`. This makes it easier to audit later.
Tools to Help
There are several tools that can assist with flag cleanup:
- LaunchDarkly: Provides a dashboard with last-used timestamps and flag activity logs.
- OpenFeature: An open standard that can be integrated with custom tooling.
- Flipt: Self-hosted flag management with audit trails.
- Custom scripts: Use ripgrep, jq, and your observability platform to build a flag inventory.
# Export all flag names from LaunchDarkly API
curl -s -H "Authorization: $LAUNCHDARKLY_API_TOKEN" \
"https://app.launchdarkly.com/api/v2/flags/default" | \
jq -r '.items[] | "\(.key) \(.variations[0].value) \(.temporary)"'The Bottom Line
Feature flag debt is real, and it grows faster than you think. The 45-minute outage I mentioned earlier could have been avoided if someone had simply removed the flag when the feature was stable. But no one did, because flag cleanup is always the thing you'll do next sprint.
Don't let it be. Start with an audit today. Remove one dead flag this week. Automate the rest. Your future self — and your on-call engineer — will thank you.
Frequently asked questions
How do I identify dead feature flags in production?
Start with a combination of static analysis (grep for flag names in code) and runtime metrics (log usage of flag evaluation). Check if the flag's toggle is set to 100% or 0% for all users — if so, it's likely dead. Also look for flags that haven't been modified in 6+ months.
What's the best way to remove a feature flag from code?
Remove all conditional branches for the flag, keeping the code path that matches the intended permanent behavior. For a flag that's fully rolled out, keep the 'true' branch and delete the 'false' branch. For a flag that's rolled back, do the opposite. Then run your full test suite and verify no references remain.
How can I prevent feature flag debt from accumulating?
Implement a flag lifecycle policy: every flag must have an owner and an expiration date. Use automated checks in CI to fail builds if a flag exceeds its expiration. Also require code reviews to include a cleanup plan for new flags.
What tools can help with feature flag cleanup?
Static analysis tools like ripgrep or custom scripts can find flag references. Feature flag systems like LaunchDarkly or OpenFeature offer dashboards showing last-used timestamps. For runtime detection, use structured logging for every flag evaluation and aggregate results in your observability platform.