What Are Feature Flags? How to Ship Code Safely When You're Building with AI
You asked Claude to add a new feature. It built the whole thing. But now you're terrified to deploy it because your app is live and people are using it. Feature flags let you ship the code and turn the feature on later — or turn it off instantly if something breaks.
TL;DR
Feature flags are if/else checks in your code that control whether a feature is active — without changing or redeploying the code. You deploy the new feature wrapped in a flag. When you're ready, you flip the flag to "on." If something breaks, you flip it back to "off." The code stays deployed. Only the behavior changes. Think of it like a light switch for features inside your app.
Why AI Coders Need to Understand Feature Flags
Here's a situation every vibe coder hits eventually: you've got a working app. Real users. Maybe it's a SaaS tool, a community platform, or a client project. You ask Claude to build a new feature — say, a redesigned dashboard or a new payment flow. Claude builds the whole thing. It looks great in development.
Now what?
If you just deploy it, you're crossing your fingers and hoping nothing breaks. If the new dashboard has a bug, every single user sees it. Your only option is to rush a fix or roll back the entire deployment. That's stressful when you're a solo builder without a QA team.
Feature flags solve this by separating deployment from release. Those are two different things, and understanding the difference changes how you ship code:
- Deployment = pushing code to your server. The code is there, but nobody sees the new behavior yet.
- Release = turning the feature on so users experience it.
With feature flags, you can deploy on Monday, test in production on Tuesday, turn it on for 10% of users on Wednesday, and fully release on Friday. Or you can turn it off in 3 seconds on Thursday night when you notice a problem — without touching your server.
Why this matters for AI-built code: When AI generates a feature, you didn't write every line. You might not fully understand every edge case. Feature flags give you a safety net — a way to test AI-generated code in production with real users, without betting the whole app on it working perfectly the first time.
According to a 2024 LaunchDarkly survey, 80% of high-performing engineering teams use feature flags as part of their deployment process. It's not a nice-to-have — it's how professional software gets shipped. And you can use the same approach even as a solo builder.
Real Scenario: You're Adding a New Pricing Page
Let's say you've been running your app with a simple pricing page. Now you want to test a completely new pricing layout — different tiers, different copy, maybe a toggle between monthly and annual billing. You prompt Claude:
I have an Express.js app with a pricing page at /pricing. I want to build a new version of the pricing page but I don't want to show it to all users yet. Set up a feature flag system so I can toggle between the old and new pricing page. Use a simple JSON config file for the flags — no external service needed. Include the flag check in the route handler and a way to update flags without restarting the server.
Claude will generate a feature flag system. Let's look at what it builds and break down every piece.
What AI Generated
Here's a typical output when you ask Claude for a feature flag setup. This is simplified but realistic — it covers the core pattern that scales from side project to production app.
1. The Feature Flags Config File
// config/feature-flags.json
{
"new_pricing_page": {
"enabled": false,
"description": "Redesigned pricing page with annual toggle",
"created": "2026-03-26",
"remove_by": "2026-04-26"
},
"dark_mode": {
"enabled": true,
"description": "Dark mode theme option",
"created": "2026-03-10",
"remove_by": "2026-04-10"
},
"ai_chat_widget": {
"enabled": false,
"description": "AI-powered chat support widget",
"created": "2026-03-26",
"remove_by": "2026-05-01"
}
}
2. The Feature Flag Module
// lib/featureFlags.js
const fs = require('fs');
const path = require('path');
const FLAGS_PATH = path.join(__dirname, '..', 'config', 'feature-flags.json');
// Cache flags in memory, reload from file periodically
let flagsCache = {};
let lastLoaded = 0;
const RELOAD_INTERVAL = 5000; // Check for changes every 5 seconds
function loadFlags() {
const now = Date.now();
if (now - lastLoaded < RELOAD_INTERVAL && Object.keys(flagsCache).length > 0) {
return flagsCache;
}
try {
const raw = fs.readFileSync(FLAGS_PATH, 'utf-8');
flagsCache = JSON.parse(raw);
lastLoaded = now;
} catch (err) {
console.error('Failed to load feature flags:', err.message);
// If file is broken, keep using cached version
}
return flagsCache;
}
function isEnabled(flagName) {
const flags = loadFlags();
const flag = flags[flagName];
if (!flag) {
console.warn(`Feature flag "${flagName}" not found. Defaulting to false.`);
return false;
}
return flag.enabled === true;
}
function getAllFlags() {
return loadFlags();
}
function setFlag(flagName, enabled) {
const flags = loadFlags();
if (!flags[flagName]) {
throw new Error(`Feature flag "${flagName}" does not exist`);
}
flags[flagName].enabled = enabled;
fs.writeFileSync(FLAGS_PATH, JSON.stringify(flags, null, 2));
flagsCache = flags;
lastLoaded = Date.now();
return flags[flagName];
}
module.exports = { isEnabled, getAllFlags, setFlag };
3. Using the Flag in a Route
// routes/pricing.js
const express = require('express');
const router = express.Router();
const { isEnabled } = require('../lib/featureFlags');
router.get('/pricing', (req, res) => {
if (isEnabled('new_pricing_page')) {
// New pricing page
return res.render('pricing-v2', {
title: 'Pricing',
plans: getNewPricingPlans()
});
}
// Original pricing page (default)
res.render('pricing', {
title: 'Pricing',
plans: getOriginalPricingPlans()
});
});
function getOriginalPricingPlans() {
return [
{ name: 'Starter', price: 9, features: ['5 projects', 'Basic support'] },
{ name: 'Pro', price: 29, features: ['Unlimited projects', 'Priority support'] }
];
}
function getNewPricingPlans() {
return [
{ name: 'Free', price: 0, features: ['2 projects', 'Community support'] },
{ name: 'Builder', price: 19, features: ['20 projects', 'Email support'] },
{ name: 'Pro', price: 49, features: ['Unlimited', 'Priority support', 'API access'] }
];
}
module.exports = router;
4. Admin Endpoint to Toggle Flags
// routes/admin.js
const express = require('express');
const router = express.Router();
const { getAllFlags, setFlag } = require('../lib/featureFlags');
// View all flags
router.get('/admin/flags', (req, res) => {
const flags = getAllFlags();
res.json(flags);
});
// Toggle a flag
router.post('/admin/flags/:name', (req, res) => {
const { name } = req.params;
const { enabled } = req.body;
if (typeof enabled !== 'boolean') {
return res.status(400).json({
error: 'Request body must include "enabled" as a boolean'
});
}
try {
const updated = setFlag(name, enabled);
res.json({ flag: name, ...updated });
} catch (err) {
res.status(404).json({ error: err.message });
}
});
module.exports = router;
Understanding Each Part
If you've never seen a feature flag system before, here's what each piece does and why it exists.
The Config File — Your Source of Truth
The feature-flags.json file is where all your flags live. Each flag has:
enabled— the actual switch.truemeans the feature is on,falsemeans it's off.description— a human-readable note so you (or future-you) know what this flag controls. This seems minor but trust me: after three months you won't remember whatnew_pricing_pagerefers to.created— when you added the flag.remove_by— a reminder to clean up the flag after the feature is fully rolled out. This is the part most people skip and then regret.
Why a JSON file? It's the simplest approach that works. No database queries, no external service, no API key. You edit a file and the change takes effect within seconds. For solo projects and small teams, this is all you need. When you outgrow it, you can move flags to a database or service like LaunchDarkly or Unleash.
The Feature Flag Module — Reading and Writing Flags
The featureFlags.js module is the brain of the system. Here's what matters:
- Caching with reload — It doesn't read the JSON file on every single request. It caches the flags in memory and only re-reads the file every 5 seconds. This means your app stays fast, but changes to the file take effect within seconds.
isEnabled(flagName)— The function you call everywhere. Give it a flag name, get backtrueorfalse. If the flag doesn't exist, it returnsfalse(safe default) and logs a warning.- Error recovery — If the JSON file gets corrupted (bad edit, syntax error), the module keeps using the last good cached version instead of crashing your app.
setFlag()— Updates the flag in both the file and the cache. This is what the admin endpoint calls.
The Route Handler — Where the Flag Does Its Job
The pricing route is where you see feature flags in action. It's a simple pattern:
if (isEnabled('new_pricing_page')) {
// Show the new version
} else {
// Show the old version
}
That's it. That's the whole pattern. Every feature flag in the world works this way — the complexity is in how you manage the flags, not how you use them in your code.
Notice the old pricing page is the default. If the flag doesn't exist, if the config file is broken, if anything goes wrong — users see the known-working version. This is called a safe default, and it's the most important design decision in the entire system.
The Admin Endpoint — Your Remote Control
The admin routes let you check flag status and toggle them without SSH-ing into your server or editing files by hand. In production, you'd hit:
# Check all flags
curl http://yourapp.com/admin/flags
# Turn on the new pricing page
curl -X POST http://yourapp.com/admin/flags/new_pricing_page \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
# Oh no, something's wrong — turn it off
curl -X POST http://yourapp.com/admin/flags/new_pricing_page \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
From "something's broken" to "it's turned off" in one HTTP request. No redeploy. No rollback. No downtime. That's the power of feature flags.
⚠️ Security note: The admin endpoint above has no authentication. In production, you must protect it. Add middleware that checks for an API key, a session, or restrict it to internal network access only. See our guide on API key management for how to secure admin endpoints.
What AI Gets Wrong About Feature Flags
AI is great at generating the basic pattern, but there are consistent blind spots you need to watch for.
1. No Cleanup Strategy
AI will generate flags that work perfectly — and then leave them in your code forever. After six months, you'll have 20 flags scattered across your codebase, half of them permanently set to true. Nobody knows which ones are safe to remove.
What to do: Always include remove_by dates. Set a monthly reminder to audit your flags. When a feature is fully rolled out and stable (2-4 weeks at 100%), remove the flag and the old code path entirely.
2. Missing the "Off" State
AI sometimes generates a feature with a flag but doesn't test the flag-off path. The new code works great when the flag is on, but when you turn it off, the old code path has a subtle bug because AI modified something both paths depend on.
What to do: After AI generates a feature-flagged feature, test with the flag off first. Make sure the existing behavior still works exactly as before. Then test with it on.
3. Hardcoded Instead of Configurable
Sometimes when you ask for a feature flag, AI generates something like this:
// This is NOT a feature flag
const SHOW_NEW_PRICING = true; // Change to false to disable
That's just a constant. You have to change the code and redeploy to toggle it. A real feature flag reads from an external source (file, database, environment variable) so you can change it without changing code.
4. No Default Value Handling
AI-generated flag checks sometimes look like this:
if (flags.new_pricing_page.enabled) { // 💥 crashes if flag doesn't exist
showNewPage();
}
If someone deletes the flag from the config or misspells it, the whole app crashes. Proper feature flags always handle missing flags gracefully:
if (isEnabled('new_pricing_page')) { // ✅ returns false if missing
showNewPage();
}
5. Feature Flags for Permanent Configuration
AI sometimes uses feature flags for things that should be regular config — like which database to connect to, or whether to use HTTPS. Feature flags are for temporary feature rollouts. Permanent settings belong in environment variables or config files. If a "flag" will never be removed, it's not a flag — it's configuration.
How to Debug Feature Flags with AI
Feature flags introduce a unique category of bugs: state-dependent behavior. Your app does different things depending on flag state, and that can be confusing to debug. Here are the most common issues and exactly what to tell your AI.
"The feature works in dev but not in production"
My feature flag "new_pricing_page" is set to true in my local feature-flags.json but the feature isn't showing in production. Here's my featureFlags.js module and my route handler. Can you check: (1) is the production config file being read from the right path, (2) is there a caching issue, (3) is the flag name spelled exactly the same everywhere?
Common causes:
- The production config file still has the flag set to
false(you edited local but forgot to deploy the config) - The file path resolves differently in production (relative vs. absolute paths)
- A typo:
new_pricing_pagein the config butnewPricingPagein the code
"I turned the flag off but users still see the feature"
I turned off a feature flag but users are still seeing the feature. Here's my flag-reading code. Is there a caching layer, CDN, or browser cache that could be serving the old response? Also check if there's a race condition where the flag is read once at startup and never re-checked.
Common causes:
- Server-side caching (the flag module caches and hasn't reloaded yet)
- CDN or browser caching the HTML/response from when the flag was on
- The flag was read at server startup and stored in a variable (never re-reads the file)
- Multiple server instances and only one got the updated config
"The app crashes when I add a new flag"
I edited feature-flags.json to add a new flag and now my app crashes with "Unexpected token" error. The flag-reading module can't parse the JSON. Can you validate my JSON file and check for trailing commas, missing quotes, or other syntax issues?
Common cause: JSON syntax errors. JSON is unforgiving — a trailing comma, a missing quote, or a comment (// like this) will break it. Use a JSON validator or ask Claude to check the file.
Pro tip: Add a health check endpoint that reports the current state of all flags. When debugging, hit /admin/flags first to see what the server actually thinks the flags are — not what you think they should be.
Beyond On/Off: Feature Flag Patterns You'll See
The simple boolean flag covers 80% of use cases. But as your app grows, you'll encounter these patterns. You don't need to implement them now — just know they exist so you can ask AI for them when you're ready.
Percentage Rollouts
Instead of on/off for everyone, you show the feature to a percentage of users. "Turn it on for 10% of users, watch for errors, then bump to 50%, then 100%." This is how big companies like Netflix and Facebook ship every feature.
function isEnabledForUser(flagName, userId) {
const flag = getFlag(flagName);
if (!flag || !flag.enabled) return false;
if (!flag.percentage) return true; // No percentage = 100%
// Use userId to deterministically assign a percentage bucket
const hash = simpleHash(userId + flagName);
const bucket = hash % 100;
return bucket < flag.percentage;
}
The key detail: you hash the user ID so the same user always gets the same experience. User #42 doesn't see the new pricing page, then see the old one on refresh, then the new one again. That would be confusing.
Kill Switches
A feature flag that's specifically designed to disable something fast. When your error monitoring shows a spike, you hit the kill switch. This is different from a rollout flag — kill switches are for emergencies and should be checked as early in the request as possible.
// At the top of your request handler
if (isEnabled('kill_switch_payments')) {
return res.status(503).json({
error: 'Payment processing is temporarily unavailable',
retryAfter: 300
});
}
Feature Flags vs. Other Approaches
Feature flags aren't the only way to ship safely. Here's how they compare to other approaches you'll encounter:
| Approach | What It Does | Best For |
|---|---|---|
| Feature Flags | Toggle features on/off at runtime | Gradual rollouts, kill switches, A/B testing |
| Blue-Green Deployment | Run two identical environments, switch traffic between them | Zero-downtime deploys, instant rollback of entire releases |
| API Versioning | Run multiple API versions simultaneously | Public APIs where clients can't update instantly |
| Environment Variables | Configure behavior via server environment | Permanent settings that change between environments (dev/staging/prod) |
These aren't competing approaches — they complement each other. A mature deployment pipeline uses feature flags and blue-green deployment and API versioning. You start with feature flags because they're the simplest and solve the most common problem: "I want to ship this but I'm not sure it works."
Where Feature Flags Fit in Your Stack
Feature flags touch multiple parts of your application. Understanding where they fit helps you design them well:
- Backend routes — The most common place. Serve different responses based on flags. The examples above live here.
- Frontend components — Show or hide UI elements. Pass flag state from the server to the client, or check flags via an API call.
- API responses — Include or exclude fields from API responses. Useful when you're changing your API version gradually.
- Background jobs — Enable or disable processing pipelines or worker tasks.
- Error handling — Toggle between error handling strategies. When paired with error monitoring, flags let you enable verbose logging for debugging without affecting all users.
Start simple: You don't need feature flags on day one of a project. Add them when you have your first "I need to ship this but I'm scared" moment. That's the signal. Start with a JSON file and the isEnabled() function. You can upgrade to a database or service later.
What to Learn Next
Feature flags are one piece of shipping code safely. Here's where to go from here:
Frequently Asked Questions
What are feature flags?
Feature flags are if/else statements in your code that check whether a feature should be turned on or off. Instead of deploying new code to enable a feature, you flip a flag — usually a boolean value stored in a config file, database, or feature flag service. The code for the feature is already deployed; the flag controls whether users see it. Think of it as a light switch for individual features inside your app.
Do I need a paid service like LaunchDarkly for feature flags?
No. For most AI-built projects, a simple config file or environment variable works perfectly. Paid services like LaunchDarkly, Flagsmith, or Unleash add powerful features like percentage rollouts, user targeting, and analytics — but you don't need them until you have significant traffic and want granular control. Start with a JSON config file or environment variables. Upgrade when the simple approach becomes limiting.
What's the difference between feature flags and environment variables?
Environment variables require a restart or redeploy to change. Feature flags stored in a database, config file, or external service can be changed at runtime — no restart needed (especially with file-watching or service-based approaches). For simple on/off toggles that rarely change, environment variables work fine. For features you want to turn on and off quickly — like during an incident — use a config file or database that your app re-reads automatically.
When should I remove a feature flag?
Remove a feature flag once the feature is fully rolled out and stable — typically 2-4 weeks after 100% rollout. Old flags create technical debt: extra if/else branches, confusing code paths, and potential bugs when flag combinations interact unexpectedly. Best practice: add a remove_by date when you create the flag, and do a monthly flag cleanup where you remove any flags that have been fully enabled for more than a month.
Can feature flags cause bugs?
Yes. The most common bug is testing with the flag in one state but deploying with it in another. Other issues include: stale flags that nobody remembers the purpose of, flag combinations that create states nobody tested (flag A on + flag B off), and database-backed flags that fail silently when the database is down. Mitigations: always have a safe default (false), test both on and off states, include descriptions and removal dates, and keep the total number of active flags small.