Six months. That's how long a disabled feature flag sat in my production codebase before a new team member asked: "What does this ENABLE_LEGACY_CHECKOUT thing do?"
Nobody remembered. The feature had been replaced, the flag was permanently set to false, but the dead code path was still there — loading, parsing, and checking on every single request.
The Discovery
It started as a routine code review. A junior dev flagged a function that looked suspicious:
function processPayment(order) {
if (config.flags.ENABLE_LEGACY_CHECKOUT) {
return legacyPaymentProcessor(order);
}
return newPaymentProcessor(order);
}
The flag had been false for 187 days. Every payment went through newPaymentProcessor. But every single call still evaluated the condition, loaded the legacy module (yes, it was lazy-loaded), and ran the config lookup.
The Hidden Costs
Here's what that one dead flag was doing:
1. Performance Tax — 0.3ms per request doesn't sound like much. Multiply by 2.4 million requests per month and you're burning ~12 hours of CPU time. Not catastrophic, but it adds up across dozens of flags.
2. Cognitive Load — New developers spent an average of 20 minutes trying to understand what each flag controlled. We had 47 active flags and 12 dead ones. That's 25% of our flag registry being ghost code.
3. Testing Overhead — Our test matrix had to account for flag combinations. Dead flags meant dead test branches that nobody was maintaining. We found 3 test suites that only tested dead code paths.
4. Deployment Risk — When someone finally cleaned up the flag, they accidentally removed a config validation that was shared between legacy and new paths. Caused a 15-minute outage.
What I Did About It
1. Flag Audit Script
I wrote a quick script to scan our codebase:
# Find all feature flags and check if they're always true or always false
grep -r "config\.flags\." src/ | \
grep -o "ENABLE_[A-Z_]*" | \
sort | uniq -c | sort -rn
Then cross-referenced with our LaunchDarkly dashboard to find flags that hadn't changed state in 90+ days.
2. The "Flag Funeral" Process
For each dead flag:
- Verify — Check logs for 30 days to confirm zero true evaluations
-
Comment — Add a
@deprecatednote with removal date - Remove — Delete the code path in the next sprint
- Celebrate — Log it in our engineering changelog (yes, really)
3. Automated Cleanup Rules
We added CI checks:
- Flag unchanged for 60 days → warning in PR
- Flag unchanged for 90 days → auto-create cleanup ticket
- Flag unchanged for 120 days → block new flag creation until old ones are cleaned
The Results
After 3 weeks of cleanup:
| Metric | Before | After |
|---|---|---|
| Active feature flags | 47 | 31 |
| Dead code branches | 12 | 0 |
| Avg. flag understanding time | 20 min | 5 min |
| Test suite execution time | 14 min | 11 min |
34% reduction in flag count. 75% faster onboarding for new flags. And most importantly — zero confusion about what's live and what's dead.
The Lesson
Feature flags are like garden plants. If you don't prune them, they grow wild and start choking the things you actually care about.
Every flag you add is technical debt with an expiration date. Set the date. Enforce it.
Your future self — and your team — will thank you.
Found this useful? Follow me for more production war stories and practical devops tips.
Top comments (0)