Feature Flags Implementation: LaunchDarkly, Unleash, and Building Your Own
Implement feature flags for safe deployments, A/B testing, and gradual rollouts. Compare LaunchDarkly vs Unleash vs custom solutions with TypeScript code exampl
Feature Flags Implementation: LaunchDarkly, Unleash, and Building Your Own
Feature flags decouple deployment from release. You merge code to main, ship it to production, and then turn the feature on for 1% of users โ then 10%, then 100% โ watching metrics the whole time. If something breaks, you flip the flag off without a rollback deploy.
This matters more as teams scale. When 20 engineers are shipping code daily, every deploy is a risk surface. Feature flags let you ship continuously while releasing deliberately.
What Feature Flags Enable
| Use Case | How Flags Help |
|---|---|
| Gradual rollout | Enable for 1% โ 10% โ 50% โ 100% of users |
| Kill switch | Instantly disable a broken feature without a redeploy |
| A/B testing | Route different users to different implementations |
| Beta programs | Enable for specific user IDs or organization IDs |
| Ops flags | Toggle maintenance mode, rate limiting, circuit breakers |
| Regional rollout | Enable in US before EU, or vice versa |
| Internal testing | Enable only for employees before external release |
The Anatomy of a Feature Flag
A flag evaluation takes context (who is asking) and returns a value (what they should see):
// Simplest case โ boolean flag
const isEnabled = await flags.getBooleanValue('new-checkout-flow', userId, false);
// Multivariate โ returns one of several values
const variant = await flags.getStringValue(
'pricing-page-layout',
userId,
'control' // default
);
// Returns: 'control' | 'variant-a' | 'variant-b'
// Number flag โ useful for rate limits, thresholds
const rateLimit = await flags.getNumberValue('api-rate-limit-rpm', userId, 100);
The flag key, the user context (for targeting), and a default value (for when the flag service is unavailable) are the three required inputs to every evaluation.
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Option 1: LaunchDarkly
LaunchDarkly is the most feature-rich managed option. It has sophisticated targeting rules, a streaming SDK that pushes flag changes to running servers in milliseconds, and built-in experimentation.
Node.js SDK setup:
// lib/flags.ts
import * as ld from '@launchdarkly/node-server-sdk';
const ldClient = ld.init(process.env.LAUNCHDARKLY_SDK_KEY!, {
// Use LaunchDarkly's flag streaming (default) for instant updates
// Set offline: true for tests
});
await ldClient.waitForInitialization({ timeout: 10 });
export interface FlagUser {
key: string; // Unique user ID โ used for consistent bucketing
email?: string;
custom?: Record<string, string | boolean | number>;
}
export async function getBooleanFlag(
flagKey: string,
user: FlagUser,
defaultValue: boolean
): Promise<boolean> {
const context: ld.LDContext = {
kind: 'user',
key: user.key,
email: user.email,
...user.custom,
};
return ldClient.variation(flagKey, context, defaultValue);
}
export async function getStringFlag(
flagKey: string,
user: FlagUser,
defaultValue: string
): Promise<string> {
const context: ld.LDContext = { kind: 'user', key: user.key, email: user.email };
return ldClient.variation(flagKey, context, defaultValue);
}
// For React โ use the LaunchDarkly React SDK
// import { useLDClient, useFlags } from 'launchdarkly-react-client-sdk';
Middleware to attach flags to request context:
// middleware/featureFlags.ts
import { FastifyRequest, FastifyReply } from 'fastify';
import { getBooleanFlag, getStringFlag } from '@/lib/flags';
declare module 'fastify' {
interface FastifyRequest {
flags: {
newCheckoutFlow: boolean;
pricingVariant: string;
apiRateLimit: number;
};
}
}
export async function featureFlagMiddleware(
request: FastifyRequest,
reply: FastifyReply
) {
const userId = request.headers['x-user-id'] as string ?? 'anonymous';
const userEmail = request.headers['x-user-email'] as string;
const user = { key: userId, email: userEmail };
// Evaluate all flags needed for this request in parallel
const [newCheckoutFlow, pricingVariant, apiRateLimit] = await Promise.all([
getBooleanFlag('new-checkout-flow', user, false),
getStringFlag('pricing-page-variant', user, 'control'),
getBooleanFlag('increased-rate-limit', user, false),
]);
request.flags = {
newCheckoutFlow,
pricingVariant,
apiRateLimit: apiRateLimit ? 500 : 100,
};
}
Option 2: Unleash (Self-Hosted)
Unleash is open-source and self-hosted โ you own the flag server, the data never leaves your infrastructure, and there's no per-seat or per-request pricing. The tradeoff is operational overhead and a less polished UI.
Deploying Unleash on Docker:
# docker-compose.yml for Unleash
version: '3.8'
services:
unleash-db:
image: postgres:16
environment:
POSTGRES_DB: unleash
POSTGRES_USER: unleash
POSTGRES_PASSWORD: ${UNLEASH_DB_PASSWORD}
volumes:
- unleash-data:/var/lib/postgresql/data
unleash:
image: unleashorg/unleash-server:latest
environment:
DATABASE_URL: postgresql://unleash:${UNLEASH_DB_PASSWORD}@unleash-db/unleash
INIT_FRONTEND_API_TOKENS: default:development.unleash-insecure-frontend-api-token
INIT_CLIENT_API_TOKENS: default:development.unleash-insecure-api-token
ports:
- "4242:4242"
depends_on:
- unleash-db
volumes:
unleash-data:
Node.js client:
// lib/unleash.ts
import { initialize } from 'unleash-client';
const unleash = initialize({
url: process.env.UNLEASH_URL!, // e.g., https://unleash.internal/api
appName: 'api-service',
customHeaders: {
Authorization: process.env.UNLEASH_API_TOKEN!,
},
// Sync interval โ how often to poll for flag changes
refreshInterval: 15, // seconds
metricsInterval: 60,
});
await new Promise<void>((resolve, reject) => {
unleash.on('synchronized', resolve);
unleash.on('error', reject);
setTimeout(reject, 10_000); // Fail fast on startup
});
export function isEnabled(
flagName: string,
userId: string,
sessionId?: string
): boolean {
return unleash.isEnabled(flagName, {
userId,
sessionId,
remoteAddress: undefined,
});
}
export function getVariant(flagName: string, userId: string): string {
const variant = unleash.getVariant(flagName, { userId });
return variant.enabled ? variant.name : 'disabled';
}
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Option 3: Build Your Own (Minimal)
For small teams or simple use cases, a custom implementation with PostgreSQL + Redis is often the right choice. No vendor dependency, no per-seat cost, full control.
// lib/customFlags.ts
import Redis from 'ioredis';
import { db } from './db';
const redis = new Redis(process.env.REDIS_URL!);
const FLAG_CACHE_TTL = 30; // seconds
interface FlagRule {
type: 'percentage' | 'user_ids' | 'email_domain' | 'always';
percentage?: number; // 0โ100 for percentage rollout
userIds?: string[]; // Allowlist of specific user IDs
emailDomain?: string; // e.g., 'viprasol.com' for internal only
}
interface FeatureFlag {
key: string;
enabled: boolean;
rules: FlagRule[];
defaultValue: boolean;
}
async function getFlagConfig(key: string): Promise<FeatureFlag | null> {
// Check cache first
const cached = await redis.get(`flag:${key}`);
if (cached) return JSON.parse(cached);
// Load from DB
const flag = await db.featureFlag.findUnique({ where: { key } });
if (!flag) return null;
const config: FeatureFlag = {
key: flag.key,
enabled: flag.enabled,
rules: flag.rules as FlagRule[],
defaultValue: flag.defaultValue,
};
await redis.setex(`flag:${key}`, FLAG_CACHE_TTL, JSON.stringify(config));
return config;
}
export async function evaluateFlag(
key: string,
userId: string,
userEmail?: string
): Promise<boolean> {
const flag = await getFlagConfig(key);
if (!flag || !flag.enabled) return flag?.defaultValue ?? false;
for (const rule of flag.rules) {
switch (rule.type) {
case 'always':
return true;
case 'user_ids':
if (rule.userIds?.includes(userId)) return true;
break;
case 'email_domain':
if (userEmail?.endsWith(`@${rule.emailDomain}`)) return true;
break;
case 'percentage':
// Consistent bucketing โ same user always gets same result
const hash = hashUserId(`${key}:${userId}`);
const bucket = hash % 100;
if (bucket < (rule.percentage ?? 0)) return true;
break;
}
}
return flag.defaultValue;
}
// Simple consistent hash (djb2)
function hashUserId(input: string): number {
let hash = 5381;
for (let i = 0; i < input.length; i++) {
hash = (hash * 33) ^ input.charCodeAt(i);
}
return Math.abs(hash);
}
Admin API to invalidate cache when a flag changes:
// api/admin/flags/[key]/route.ts
export async function PUT(request: Request, { params }: { params: { key: string } }) {
const body = await request.json();
await db.featureFlag.upsert({
where: { key: params.key },
create: { key: params.key, ...body },
update: body,
});
// Invalidate cache immediately
await redis.del(`flag:${params.key}`);
return Response.json({ success: true });
}
Comparison
| Factor | LaunchDarkly | Unleash | Custom |
|---|---|---|---|
| Setup time | 1 hour | 1โ2 days | 1โ2 weeks |
| Targeting sophistication | Excellent | Good | You build it |
| A/B testing / experiments | Built-in | Plugin | You build it |
| Real-time flag updates | < 200ms (streaming) | ~15s (poll) | Depends on impl |
| Analytics / audit log | Excellent | Good | You build it |
| Self-hosted | No | Yes | Yes |
| Data residency | LaunchDarkly's cloud | Your infra | Your infra |
| Cost | $12โ35/seat/mo | Free (self-host) | Engineering time |
| Operational overhead | None | Medium | Low |
Flag Hygiene: Avoiding Flag Debt
Flags accumulate. A codebase with 200 dead flags is a maintenance nightmare โ every if (flagEnabled(...)) branch adds cognitive overhead and testing complexity.
Rules that prevent flag debt:
- Every flag has an owner and a removal date โ set when created
- Flags live in code for at most 90 days after reaching 100% rollout
- Flag names encode intent:
new-checkout-flownotfeature-123 - Permanent flags get a different treatment: ops flags (maintenance mode, rate limits) live indefinitely but are documented separately from release flags
- CI checks for stale flags: Lint rules that error if a flag key hasn't been evaluated in N days
// eslint-plugin-custom/rules/no-stale-flags.ts
// Checks flag keys against a registry with removal dates
// Fails CI if a flag's removal date has passed
Cost Summary
| Option | Monthly Cost (10 engineers) | Notes |
|---|---|---|
| LaunchDarkly Starter | $120/mo | 3 seats, limited experiments |
| LaunchDarkly Pro | $350โ600/mo | Full targeting, experiments |
| Unleash Enterprise | $150โ300/mo | Managed hosting |
| Unleash self-hosted | $40โ80/mo (infra) | Engineering time to operate |
| Custom | $0 (infra ~$20/mo) | 2โ3 weeks to build initially |
Working With Viprasol
We implement feature flag systems as part of our deployment infrastructure work โ either integrating LaunchDarkly/Unleash or building a custom solution when client requirements (data residency, cost at scale) make that the better fit.
โ Talk to our DevOps team about deployment safety and feature flags.
See Also
- CI/CD Pipeline Setup โ the deployment pipeline that feature flags plug into
- Zero Downtime Deployment โ deployment strategies alongside feature flags
- A/B Testing and SaaS Metrics โ measuring flag impact with the right metrics
- DevOps Best Practices โ the broader DevOps context
- Cloud Solutions โ infrastructure and deployment services
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.