Feature flags are one of those tools that feel like a superpower right up until the moment your codebase looks like a haunted house of dead conditionals nobody dares to delete. Done right, they let you decouple deployment from release, run controlled rollouts, and kill bad features in seconds. Done wrong, they become a maintenance tax that compounds silently until a new engineer asks "can we remove this?" and nobody knows the answer.
This article is about doing them right — from flag creation through rollout to cleanup — with concrete Node.js patterns and a setup that does not require a $2,000/month SaaS subscription.
What a Feature Flag Actually Is (and Is Not)
A feature flag is a conditional that controls whether a code path is active for a given user, environment, or percentage of traffic. It is not a configuration value, an environment variable, or a permanent A/B test. The key word is temporary. Every flag you create should have a predetermined expiry condition: "this flag is removed once the feature is fully rolled out and stable for 14 days."
Treating flags as permanent is the root cause of flag debt.
The Four Types You Will Actually Use
Before writing any code, know which type you are creating:
- Release flags — hide unfinished features behind a toggle; removed after full rollout.
- Experiment flags — drive A/B or multivariate tests; removed after statistical significance is reached.
- Ops flags — kill switches for degraded third-party services or expensive features under load.
- Permission flags — gate features by plan or role; these can be long-lived but must be documented as such.
Mixing these up — especially treating a release flag like a permission flag — is where the rot begins.
A Lightweight Self-Hosted Setup in Node.js
You do not need Unleash, LaunchDarkly, or ConfigCat to get started, though all three are excellent at scale. A Redis-backed flag store with a thin evaluation layer gets you 90% of the way there for most early-stage products.
The Flag Schema
Store each flag as a JSON object:
{
"flagKey": "new_checkout_flow",
"type": "release",
"enabled": true,
"rolloutPercentage": 20,
"allowlist": ["user_123", "user_456"],
"createdAt": "2025-01-10T09:00:00Z",
"expiresAt": "2025-02-10T09:00:00Z",
"owner": "payments-team",
"ticket": "ENG-4421"
}
The expiresAt, owner, and ticket fields are non-negotiable. Without them, you cannot automate cleanup or hold anyone accountable.
The Evaluation Function
// flagClient.js
const redis = require('redis');
const client = redis.createClient({ url: process.env.REDIS_URL });
async function isEnabled(flagKey, userId = null) {
const raw = await client.get(`flag:${flagKey}`);
if (!raw) return false;
const flag = JSON.parse(raw);
if (!flag.enabled) return false;
// Allowlist takes priority
if (userId && flag.allowlist?.includes(userId)) return true;
// Percentage rollout using consistent hashing
if (flag.rolloutPercentage < 100 && userId) {
const hash = simpleHash(userId + flagKey) % 100;
return hash < flag.rolloutPercentage;
}
return flag.rolloutPercentage === 100;
}
function simpleHash(str) {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = (hash * 33) ^ str.charCodeAt(i);
}
return Math.abs(hash);
}
module.exports = { isEnabled };
The consistent hashing on userId + flagKey ensures the same user always gets the same experience — critical for avoiding flickering between requests.
Using It in a Route
const { isEnabled } = require('./flagClient');
router.post('/checkout', async (req, res) => {
const useNewFlow = await isEnabled('new_checkout_flow', req.user.id);
if (useNewFlow) {
return newCheckoutController(req, res);
}
return legacyCheckoutController(req, res);
});
Clean, auditable, and easy to grep for when it is time to delete the flag.
The Rollout Lifecycle
A disciplined lifecycle has four phases:
1. Creation — Flag starts at 0% or allowlist-only. Developer and ticket are recorded. Expiry is set. Flag is announced in the team's deployment channel.
2. Gradual rollout — Increment in steps: 5% → 20% → 50% → 100%. Monitor error rates, latency, and business metrics at each step. Automate alerts that compare flag-on vs. flag-off cohorts.
3. Full release — Flag reaches 100% with no allowlist exceptions. The feature is now "live" even though the flag still exists. Start the cleanup clock here.
4. Cleanup — After a stability window (7–14 days is typical), the flag is deleted from the store and the conditional is removed from the codebase. The dead code is gone. The ticket is closed.
Preventing Flag Debt: Operational Patterns That Work
Flag debt is an organisational problem as much as a technical one. These patterns address both:
- Automated expiry alerts — A daily cron job scans all flags, finds any with
expiresAtin the past, and posts a Slack message to the owner. No manual tracking required. - Flag inventory in your CI pipeline — A lint step fails the build if a flag is checked into code without a corresponding entry in the flag store. Orphaned conditionals cannot ship.
- Quarterly flag audits — Every three months, the team reviews all long-lived permission flags. If nobody can justify why a flag still exists, it is scheduled for removal.
- One flag per ticket rule — No flag is created without a linked issue that includes an acceptance criterion for removal. "Flag removed from codebase" is a required subtask, not an afterthought.
When to Graduate to a Managed Service
The self-hosted Redis approach works well up to roughly 50–100 active flags and a handful of services. Past that threshold — especially if you have multiple platforms (web, mobile, backend), need real-time streaming updates, or want built-in analytics on flag exposure — a managed platform earns its cost. Unleash (open-source, self-hostable at scale) and Flagsmith are strong options that avoid full vendor lock-in while giving you a proper admin UI, audit logs, and SDK support across languages.
Why This Matters for Your Project
If your team ships more than once a week, feature flags are the difference between a deployment and a release being two separate events you control deliberately. That separation is the foundation of safe continuous delivery — you can merge unfinished work to main, test it in production with internal users, and roll out to customers only when you are ready. The teams that scale without chaos are not the ones that deploy less; they are the ones that deploy often and control exposure precisely. Building that discipline early, before your flag list hits triple digits, is the engineering investment that pays off every single time.




