Feature Flags Done Right: OpenFeature, Unleash, and a Zero-Downtime Rollout Playbook
Shipping code to production on Friday afternoon used to be a rite of passage. Today, it's a résumé-generating event. Over the last six years building production systems for clients across fintech, SaaS, and e-commerce, I've learned one truth above all: the deploy button is not the release button. Feature flags are the lever that separates those two, and when you wire them up correctly, you get safer rollouts, instant kill switches, and the ability to test in production without the panic.
In this guide, I'll walk you through a battle-tested feature flag setup using OpenFeature (the vendor-neutral standard) and Unleash (a powerful self-hosted flag service). We'll also cover the rollout playbook I use on real projects — canary releases, percentage-based progressive delivery, and kill switches that actually save you at 2 AM.
Why Feature Flags Are No Longer Optional
If you're still doing all-or-nothing deployments, you're not alone — but you're leaving reliability on the table. Feature flags give you:
- Decoupling deploy from release: Code ships dark, gets activated later.
- Targeted rollouts: Ship to internal users, then 1%, then 10%, then everyone.
- Instant rollback: Flip a flag instead of redeploying.
- A/B testing infrastructure: Compare variants without forking the codebase.
- Kill switches: Wrap risky features behind a flag you can disable in seconds.
I've personally used flags to ship a payment provider migration to 5% of traffic, watch error rates spike in real time, and roll back before a single customer support ticket came in. That single feature paid for every hour I spent setting up the system.
Meet OpenFeature: The Vendor-Neutral Standard
Before we touch code, let's talk about OpenFeature. It's a CNCF incubating project that gives you a single, provider-agnostic API for feature flags. The killer benefit: you can swap flag providers (Unleash, LaunchDarkly, Flagsmith, your own DB) without rewriting application code.
Here's what the API looks like in Node.js:
import { OpenFeature } from '@openfeature/server-sdk';
import { UnleashProvider } from '@openfeature/unleash-provider';
// Register Unleash as the provider
const provider = new UnleashProvider({
url: 'http://localhost:4242/api/',
appName: 'checkout-service',
apiKey: process.env.UNLEASH_API_KEY,
});
await OpenFeature.registerProvider(provider);
const client = OpenFeature.getClient();
// Evaluate a flag
const showNewCheckout = client.getBooleanValue('new-checkout', false);
if (showNewCheckout) {
// new code path
} else {
// legacy code path
}
Notice the default value (false in this case). This is critical: if your flag service goes down or is unreachable, the default keeps your app running safely. Never let a flag service outage take down production.
Self-Hosting Unleash with Docker
Unleash has a generous open-source tier that's more than enough for most teams. I self-host it on a small VPS using Docker Compose — here's the minimal setup:
# docker-compose.yml
version: "3.9"
services:
unleash:
image: unleashorg/unleash-server:latest
ports:
- "4242:4242"
environment:
DATABASE_URL: "postgres://unleash:unleash@db/unleash"
UNLEASH_URL: "http://localhost:4242"
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_USER: unleash
POSTGRES_PASSWORD: unleash
POSTGRES_DB: unleash
volumes:
- unleash-db:/var/lib/postgresql/data
volumes:
unleash-db:
Run docker compose up -d, then head to http://localhost:4242. Create a project, generate an API token, and you're ready to define flags. For production, I'd recommend putting this behind HTTPS with Caddy or Nginx, and backing up the Postgres volume nightly — the flag state is part of your production state.
Defining Flags That Won't Bite You Later
A flag without a removal plan is technical debt. Every flag I create follows three rules:
- Name it after the behavior, not the implementation.
new-checkoutis better thanuse-stripe-element-v2. - Add an owner and a removal date in the description. I tag flags
#cleanup-2024-Q4so they show up in audits. - Default to the safest value. New flags default to
false(or the legacy path) until you're ready.
In Unleash, you can configure activation strategies directly in the UI:
- Default: on/off for everyone
- UserID: targeted list of user IDs
- Gradual rollout: percentage-based with stickiness
- Constraints: country, email domain, custom properties
The gradual rollout strategy is the one you'll use most. It supports sticky bucketing so a user always lands in the same variant — critical for consistent experiences.
The Zero-Downtime Rollout Playbook
Here's the exact sequence I follow on every non-trivial feature. It's saved me from at least three production incidents in the past year.
Step 1: Ship Dark, Toggle Off
Merge the feature behind a flag, deploy to production, leave the flag off for everyone. The code path is exercised in staging via test flags, but in production it's dormant. Run your smoke tests, your load tests, your chaos tests — all of them see the flag as off.
Step 2: Internal Canary
Enable the flag for users with the email domain @yourcompany.com or a custom internal property. This catches obvious bugs without any customer risk. I usually leave this on for 24–48 hours while the team actually uses the app.
In Unleash, add a constraint:
email endsWith @yourcompany.com
Step 3: Progressive Percentage Rollout
Now the fun part. Ramp to 1% for one hour, monitoring error rates, latency, and conversion. If the metrics look healthy, push to 5%, then 25%, then 50%, then 100%. I automate this with a simple script that reads metrics and updates the flag, but a manual runbook works fine too.
The key insight: each step should be long enough to gather statistically meaningful data. For high-traffic apps, an hour is enough. For low-traffic apps, you may need a day at each stage.
Step 4: Bake Time, Then Cleanup
Once you're at 100%, the flag stays in for one to two weeks. Why? Because some bugs only show up over time — memory leaks, edge cases in long-lived sessions, data drift. After the bake period, remove the flag and the legacy code path in a single PR.
Building a Kill Switch You'll Actually Use
A kill switch is a flag that exists solely to turn a feature off fast. It should be:
- Always evaluated — no caching, no offline mode override
- In a separate "emergency" project with restricted write access
- Documented in the runbook with exact flag names and toggle steps
I wrap risky integrations (new payment providers, ML model calls, third-party APIs) behind a kill switch flag from day one. Here's a pattern I like:
async function processPayment(order: Order) {
const useNewProvider = client.getBooleanValue('use-new-payment-provider', false);
try {
if (useNewProvider) {
return await newPaymentProvider.charge(order);
}
return await legacyPaymentProvider.charge(order);
} catch (err) {
// Telemetry: which path failed?
metrics.increment('payment.failure', { variant: useNewProvider ? 'new' : 'legacy' });
throw err;
}
}
If the new provider starts failing, you flip one toggle in Unleash, and every subsequent request falls back to the legacy provider. No deploy, no rollback commit, no all-hands meeting.
OpenFeature in a Polyglot Stack
One of the underrated wins of OpenFeature is the consistent API across languages. Your Node.js backend, Go service, Python worker, and React frontend all evaluate flags the same way:
// Go
client := openfeature.NewClient()
showNewCheckout, _ := client.BooleanValue("new-checkout", false, nil)
# Python
from openfeature import api
client = api.get_client()
show_new_checkout = client.get_boolean_value("new-checkout", False)
// React frontend
import { useFlag } from '@openfeature/react-sdk';
function CheckoutButton() {
const showNewCheckout = useFlag('new-checkout', false);
return showNewCheckout ? <NewCheckout /> : <LegacyCheckout />;
}
This means your rollout strategy stays consistent even as your stack evolves. I can't tell you how many times this has mattered on client projects where the team added a new service in a different language mid-project.
Common Pitfalls and How to Avoid Them
After setting this up for a dozen teams, here are the mistakes I see over and over:
- Flag sprawl: Hundreds of stale flags polluting the UI. Fix: scheduled cleanup sprints, ownership tags, and a "last evaluated" metric you can sort by.
- Caching too aggressively: A flag change should propagate in seconds, not minutes. Configure short TTLs on the SDK cache.
- Forgetting defaults: If your flag service is down, the app should still run — on the default. Test this by unplugging Unleash.
- Using flags for config: Feature flags and config are different problems. Don't store API keys in your flag service.
- No metrics per variant: You can't optimize what you can't measure. Always tag your telemetry with the active variant.
Wrapping Up
Feature flags aren't a luxury anymore — they're table stakes for any team that ships faster than once a month. The combination of OpenFeature for vendor neutrality and Unleash for a self-hosted, open-source backend gives you everything you need without monthly SaaS bills or lock-in. Add a disciplined rollout playbook — dark deploy, internal canary, progressive percentage, bake time, cleanup — and you have a release process that scales from a side project to a hundred-engineer org.
If you're building anything that customers depend on, set this up before you need it. The first time a kill switch saves you from a 2 AM incident, you'll know it was worth the afternoon it took to wire up.
Got questions about a specific rollout scenario or want me to dive deeper into OpenFeature's evaluation context? Drop a comment below or reach out — I help teams design release systems that actually work under pressure.
FAQ: Feature Flags, OpenFeature, and Unleash
What is the difference between OpenFeature and Unleash?
OpenFeature is a vendor-neutral specification and SDK standard for evaluating feature flags — think of it as an API contract. Unleash is one of many flag management backends (providers) that OpenFeature-compatible SDKs can talk to. You use OpenFeature in your application code and plug in Unleash as the provider; you can swap Unleash for LaunchDarkly, Flagsmith, or a custom backend later without changing application code.
Is Unleash free for production use?
Yes. Unleash has a fully open-source core (Apache 2.0) that includes unlimited flags, projects, and environments. Their commercial offering adds enterprise features like SSO, audit logs, and support, but the self-hosted open-source version is production-grade and used by companies at serious scale.
How do I prevent feature flag technical debt?
Three practices work: (1) tag every flag with an owner and a removal date, (2) run a monthly "flag cleanup" sprint where you delete anything older than 30 days post-100% rollout, and (3) instrument your app to log "last evaluated at" timestamps so you can find flags nobody's reading anymore.
Can I use OpenFeature with multiple providers at once?
Yes, OpenFeature supports a layered approach. You can stack providers — for example, a local in-memory provider for tests, with Unleash as the fallback in production. You can also have per-domain providers so one team uses Unleash and another uses LaunchDarkly within the same application.
What's the safest default value for a new feature flag?
The default value is what your app uses when the flag service is unreachable or the flag doesn't exist. Always default to the safer, more conservative behavior — usually the existing legacy code path. That way, an outage in your flag infrastructure cannot accidentally enable a half-built feature.
How long should a percentage rollout stage last?
It depends on your traffic volume. High-traffic apps (millions of requests per day) can move through 1% → 5% → 25% → 100% in a few hours. Low-traffic apps (thousands of requests per day) should hold each stage for at least 24 hours to gather meaningful error rate and latency data. The goal is enough samples to detect a 2x regression with statistical confidence.