· 8 min read
Why I Use Feature Flags for Everything
Feature flags aren't just for A/B tests. They're a deployment safety net, a debugging tool, and the key to shipping faster with less risk.
By Tanner Goins

The scariest moment in software development isn’t writing the code. It’s deploying it.
No matter how much you test, there’s always a gap between “works in staging” and “works in production.” Different data, different scale, different users doing things you didn’t anticipate. And when something goes wrong, your options are usually: fix forward under pressure, or roll back the entire deployment.
Feature flags give you a third option: turn it off.
Not the whole deployment. Not the whole service. Just the specific feature that’s causing problems. One toggle, and the broken feature disappears while everything else keeps running.
That’s the basic pitch. But after using feature flags extensively across side projects and client work, I’ve found they change how I think about shipping software in ways that go far beyond emergency kill switches.
What Feature Flags Actually Are
A feature flag is a conditional that controls whether a piece of functionality is active. At its simplest:
if (flags.newCheckoutFlow) {
renderNewCheckout();
} else {
renderOldCheckout();
} The value of flags.newCheckoutFlow isn’t hardcoded. It comes from a configuration system (a database, a config file, or a feature flag service) and can be changed without deploying code.
That’s the entire concept. Everything else is an application of this simple pattern.
Beyond the Kill Switch
Gradual Rollouts
Instead of deploying a feature to all users at once, roll it out gradually:
- Day 1: Enable for internal team only
- Day 3: Enable for 5% of users
- Day 5: If metrics look good, increase to 25%
- Day 7: Full rollout
At each step, you’re watching for errors, performance regressions, or user complaints. If something surfaces at 5%, you’ve affected a fraction of your users instead of all of them.
This is the same canary release pattern I use for prompt deployments, applied to features. The principle is identical: reduce the blast radius of changes.
Per-User Targeting
Feature flags can be targeted by user attributes: plan tier, geography, account age, or even individual user IDs. This enables patterns that would otherwise require separate code paths or entirely separate deployments:
- Beta programs. Give specific users early access to new features.
- Enterprise features. Enable premium functionality for paying customers without maintaining separate codebases.
- Internal dogfooding. Your team uses the new feature before anyone else. If it breaks, it breaks for you first.
Operational Controls
Some flags aren’t about features at all. They’re about system behavior:
- Disable an expensive background job during a traffic spike
- Switch to a read-only mode during database maintenance
- Route traffic to a fallback provider when the primary is slow
- Toggle verbose logging for a specific user who’s reporting an issue
These operational flags turn runtime decisions into configuration changes instead of code deployments. When production is on fire, the last thing you want to do is push new code.
How I Implement Feature Flags
You don’t need a fancy service to get started. Here’s the progression I’ve followed.
Level 1: Environment Variables
The simplest version. Good for solo projects and early-stage products:
const flags = {
newDashboard: process.env.FLAG_NEW_DASHBOARD === "true",
betaSearch: process.env.FLAG_BETA_SEARCH === "true",
}; Pros: Zero dependencies. Works everywhere. Free.
Cons: Requires a deployment to change. No per-user targeting. No gradual rollout.
This is where I start every project. It’s boring technology that works.
Level 2: Database-Backed Flags
Store flags in your database with a simple admin UI to toggle them:
// Simple flags table
// | name | enabled | percentage | allowed_users |
// | new_checkout | true | 25 | null |
// | beta_search | true | 100 | ["user_123"] |
async function isEnabled(flagName: string, userId?: string): Promise<boolean> {
const flag = await db.getFlag(flagName);
if (!flag || !flag.enabled) return false;
// Check user-specific override
if (flag.allowedUsers?.includes(userId)) return true;
// Percentage rollout
if (flag.percentage < 100) {
const hash = hashUserId(userId, flagName);
return hash < flag.percentage;
}
return true;
} Pros: Change flags without deploying. Per-user targeting. Percentage rollouts. All with infrastructure you already have.
Cons: You need to build the admin UI. No analytics. No audit log unless you build it.
This is my go-to for most projects. A flags table, a few helper functions, and a simple admin page is enough for most teams.
Level 3: Feature Flag Services
When you need more, like audit logs, analytics, scheduled rollouts, or complex targeting rules, a managed service makes sense:
- LaunchDarkly: the enterprise standard. Powerful, expensive.
- Flagsmith: open-source option with a hosted tier.
- Unleash: another strong open-source choice.
- PostHog: combines feature flags with analytics.
- Vercel Edge Config: if you’re on Vercel, low-latency flags at the edge.
I reach for these when a project has multiple environments, multiple team members changing flags, or compliance requirements for audit logs. For a solo developer or small team, Level 2 is usually more than enough.
Patterns I Use Daily
The Trunk-Based Development Flow
Feature flags enable trunk-based development, the practice of committing directly to main instead of using long-lived feature branches. The workflow:
- Write the new feature behind a flag (disabled by default)
- Commit to main. Deploy. The feature is in production but invisible.
- When the feature is ready, enable the flag for internal users
- Test in production with real data
- Gradual rollout to all users
- Once fully rolled out and stable, remove the flag and the old code path
This eliminates merge conflicts, long-lived branches, and the “big bang” deployment. Your code is always in main, always deployed, and always behind a flag until it’s ready.
The Feature Flag Lifecycle
Flags should have a lifecycle. A flag that’s fully rolled out and stable should be removed. Otherwise, you accumulate dead code paths that nobody maintains.
My rule: once a feature has been at 100% for two weeks with no issues, remove the flag. Add a TODO with a date when you create the flag:
// TODO: Remove flag after 2026-07-10 if stable
if (flags.newCheckoutFlow) {
// New code path
} else {
// Old code path: remove after flag cleanup
} Some teams track this more formally with expiration dates in the flag system itself. The method matters less than the discipline of cleaning up.
Testing With Flags
Feature flags interact with your test suite. Here’s how I handle it:
describe('checkout flow', () => {
it('renders new checkout when flag is enabled', () => {
setFlag('newCheckoutFlow', true);
render(<Checkout />);
expect(screen.getByText('New Checkout')).toBeInTheDocument();
});
it('renders old checkout when flag is disabled', () => {
setFlag('newCheckoutFlow', false);
render(<Checkout />);
expect(screen.getByText('Classic Checkout')).toBeInTheDocument();
});
}); Test both paths. The flag will eventually be removed, but while it exists, both code paths need to work correctly.
When Feature Flags Go Wrong
Flag Debt
The most common problem. Flags accumulate. Old flags that were fully rolled out months ago still have conditionals in the code. The old code path hasn’t been tested in weeks. Nobody remembers what the flag was for.
Prevention: Schedule flag cleanup. Track active flags. Set expiration reminders. My database-backed flags include a created_at field and I review any flag older than 30 days.
Combinatorial Complexity
Five independent flags create 32 possible combinations. Not all of those combinations have been tested. Some might interact in unexpected ways.
Prevention: Keep the total number of active flags small. In my experience, more than 10 active flags at once is a warning sign. Retire flags aggressively.
Performance Overhead
If you’re evaluating flags on every request, the lookup needs to be fast. Database queries per flag per request will add latency. Cache flag values aggressively. Most flags don’t change frequently enough to need real-time evaluation.
// Cache flags for 60 seconds
let flagCache: Map<string, Flag> = new Map();
let cacheExpiry = 0;
async function getFlags(): Promise<Map<string, Flag>> {
if (Date.now() < cacheExpiry) return flagCache;
flagCache = await db.getAllFlags();
cacheExpiry = Date.now() + 60_000;
return flagCache;
} The “Flag Everything” Trap
Not every change needs a flag. A typo fix, a CSS adjustment, a dependency update: these don’t benefit from the overhead of a feature flag. Reserve flags for changes that meaningfully affect user experience, system behavior, or business logic.
The Deployment Confidence Loop
Feature flags create a feedback loop that compounds over time:
- You deploy more often because flags reduce risk
- Smaller, more frequent deployments are easier to debug
- Issues are caught earlier because rollouts are gradual
- Your team builds confidence in the deployment process
- Higher confidence leads to shipping faster
This is the real value of feature flags: not any single deployment, but the systemic effect on how your team ships software. When deploying is safe, you deploy more. When you deploy more, each deployment is smaller. When deployments are smaller, problems are easier to find.
Compare this to the alternative: infrequent, large deployments where every release is a nerve-wracking event. Feature flags transform deployment from a scary event into a routine operation.
Start With One Flag
If you’re not using feature flags yet, start with your next feature:
- Build it behind a simple boolean flag (even an environment variable)
- Deploy it disabled
- Enable it for yourself in production
- Verify it works with real data
- Enable it for everyone
That’s it. One flag. One feature. One deployment that doesn’t keep you up at night.
Once you’ve experienced the confidence of being able to turn a feature off without a rollback, you’ll never want to deploy without flags again. They’re the structured logging of deployment. It’s a practice that seems like overhead until you need it, and then you wonder how you ever shipped without it.