Feature flags let remote teams decouple deployments from releases, run gradual rollouts, kill switches on broken features, and A/B test without coordinating deployment windows across time zones. The right tool makes the difference between flags as a discipline and flags as technical debt.
For distributed teams, feature flags solve a specific coordination problem: your engineers in Singapore should not have to wake up engineers in Berlin to roll back a bad deploy. A kill switch that anyone on-call can flip eliminates the “wake the person who built this” bottleneck entirely.
Unleash (Self-Hosted, Open Source)
Unleash is the leading open-source feature flag system. Self-hosting means no data leaves your infrastructure, which matters for regulated industries.
Deploy with Docker:
# docker-compose.yml
version: "3.8"
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: unleash
POSTGRES_USER: unleash_user
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- unleash-postgres:/var/lib/postgresql/data
unleash:
image: unleashorg/unleash-server:latest
ports:
- "4242:4242"
environment:
DATABASE_URL: postgres://unleash_user:${DB_PASSWORD}@postgres:5432/unleash
INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token"
INIT_CLIENT_API_TOKENS: "default:development.unleash-insecure-api-token"
depends_on:
- postgres
volumes:
unleash-postgres:
Use in Node.js:
import { initialize } from 'unleash-client';
const client = initialize({
url: 'https://flags.yourcompany.com/api/',
appName: 'payments-service',
customHeaders: { Authorization: process.env.UNLEASH_API_TOKEN },
});
client.on('synchronized', () => {
// SDK is ready, safe to check flags
if (client.isEnabled('new-checkout-flow')) {
runNewCheckout();
} else {
runLegacyCheckout();
}
});
Use in Go:
import "github.com/Unleash/unleash-client-go/v3"
func main() {
unleash.Initialize(
unleash.WithUrl("https://flags.yourcompany.com/api/"),
unleash.WithAppName("payments-service"),
unleash.WithCustomHeaders(http.Header{
"Authorization": []string{os.Getenv("UNLEASH_API_TOKEN")},
}),
)
if unleash.IsEnabled("new-checkout-flow", unleash.WithContext(unleash.Context{
UserId: user.ID,
})) {
runNewCheckout(user)
}
}
Unleash ships with several built-in activation strategies: gradual rollout by percentage, user ID list, IP range, and hostname. For remote teams, gradual rollout is the most valuable — you can enable a flag for 5% of users, watch error rates for 30 minutes, then dial it up to 50% and 100% without a redeployment.
// Unleash gradual rollout — configured in UI, consumed in code identically
// The SDK handles the hash-based user bucketing; no client code change needed
if (client.isEnabled('new-checkout-flow', { userId: user.id })) {
runNewCheckout();
}
Flagsmith (Open Source, SaaS or Self-Hosted)
Flagsmith supports feature flags and remote config (flags with values, not just on/off). It’s a good choice when you need both boolean flags and config values managed through the same system.
Self-hosted Docker Compose:
version: "3.8"
services:
flagsmith:
image: flagsmith/flagsmith:latest
environment:
DATABASE_URL: postgresql://flagsmith:${DB_PASSWORD}@postgres/flagsmith
ENV: production
DJANGO_ALLOWED_HOSTS: flags.yourcompany.com
SECRET_KEY: ${FLAGSMITH_SECRET_KEY}
ports:
- "8000:8000"
depends_on:
- postgres
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: flagsmith
POSTGRES_USER: flagsmith
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- flagsmith-postgres:/var/lib/postgresql/data
volumes:
flagsmith-postgres:
Python client with remote config:
import flagsmith
client = flagsmith.Flagsmith(
environment_key=os.environ["FLAGSMITH_ENV_KEY"],
api_url="https://flags.yourcompany.com/api/v1/",
)
flags = client.get_environment_flags()
# Boolean flag
if flags.is_feature_enabled("dark_mode"):
apply_dark_mode()
# Remote config value
timeout = flags.get_feature_value("api_timeout_ms")
requests.get(url, timeout=int(timeout) / 1000)
Flagsmith supports per-identity flags with traits:
identity_flags = client.get_identity_flags(
identifier=user.email,
traits={"plan": user.plan, "country": user.country}
)
if identity_flags.is_feature_enabled("beta_dashboard"):
show_beta_dashboard()
The remote config capability is particularly useful for remote teams managing multiple environments. You can store environment-specific values — timeouts, rate limits, API endpoints — in Flagsmith rather than shipping new environment variables every time a tuning parameter changes. An on-call engineer in any time zone can adjust api_timeout_ms from the Flagsmith dashboard without a deployment.
LaunchDarkly (SaaS, Enterprise)
LaunchDarkly is the industry standard for enterprise. It’s expensive but offers the most advanced targeting, experimentation, and compliance features. Worth it for teams where downtime cost > $50k/hour.
Go SDK:
import ldclient "gopkg.in/launchdarkly/go-server-sdk.v5"
import "gopkg.in/launchdarkly/go-server-sdk.v5/ldcontext"
func main() {
config := ldclient.Config{}
client, _ := ldclient.MakeCustomClient(
os.Getenv("LAUNCHDARKLY_SDK_KEY"),
config,
5*time.Second,
)
defer client.Close()
context := ldcontext.NewBuilder("user-123").
SetString("plan", "enterprise").
SetString("country", "US").
Build()
showNewUI, _ := client.BoolVariation("new-ui", context, false)
if showNewUI {
renderNewUI()
}
}
LaunchDarkly’s key differentiator is its experimentation layer — you can run A/B tests with statistical significance tracking baked in, not just flag on/off.
For enterprise remote teams, LaunchDarkly’s audit log is a major compliance advantage. Every flag change is logged with who made it, when, and from what IP. During a post-incident review, you can reconstruct exactly which flag changed and correlate it with error spikes. This is hard to replicate with a self-hosted solution without significant engineering investment.
LaunchDarkly also ships an AI feature called Accelerate that suggests when to clean up stale flags and estimates technical debt impact — useful for large teams where flag hygiene becomes a real operational problem.
OpenFeature (Vendor-Neutral SDK Standard)
OpenFeature is a CNCF standard that lets you write flag evaluation code once and swap providers without changing application code. Write once, use with any backend.
Node.js with OpenFeature:
import { OpenFeature } from '@openfeature/server-sdk';
import { UnleashProvider } from '@openfeature/unleash-provider';
// Register a provider (swap to LaunchDarkly or Flagsmith without changing flag code)
OpenFeature.setProvider(new UnleashProvider({
unleashConfig: {
url: 'https://flags.yourcompany.com/api/',
appName: 'my-service',
customHeaders: { Authorization: process.env.UNLEASH_TOKEN },
},
}));
const client = OpenFeature.getClient();
// Flag evaluation is provider-agnostic
const showFeature = await client.getBooleanValue('new-checkout', false);
const timeout = await client.getNumberValue('api-timeout-ms', 3000);
OpenFeature also supports hooks — middleware that runs before and after flag evaluation. This lets you add uniform observability across all flag checks without instrumenting each one individually:
import { OpenFeature, Hook } from '@openfeature/server-sdk';
const metricsHook: Hook = {
after(hookContext, evaluationDetails) {
metrics.increment('feature_flag.evaluation', {
flag: hookContext.flagKey,
value: String(evaluationDetails.value),
provider: hookContext.providerMetadata.name,
});
},
error(hookContext, error) {
logger.error('Feature flag evaluation failed', {
flag: hookContext.flagKey,
error: error.message,
});
},
};
OpenFeature.addHooks(metricsHook);
This approach is ideal for teams that aren’t locked in on a provider yet or anticipate switching.
Structuring Flags for Remote Teams
Bad flag naming causes confusion across time zones. Enforce a convention:
# Pattern: {type}_{service}_{description}_{ticket}
# Types: feat (feature), exp (experiment), kill (kill switch), config (remote config)
feat_checkout_new_payment_flow_ENG-1234
exp_homepage_hero_ab_test_MKT-567
kill_payments_stripe_webhook_handler_OPS-890
config_api_rate_limit_per_user_ENG-999
Document every flag in your catalog (Backstage, Confluence, or Notion):
| Flag | Type | Owner | Created | Ticket | Intended Removal |
|------|------|-------|---------|--------|------------------|
| feat_checkout_new_payment_flow | Feature | @alice | 2026-03-01 | ENG-1234 | 2026-04-15 |
| kill_payments_stripe_handler | Kill switch | @devops | 2026-02-15 | OPS-890 | Never |
For async remote teams, the “Owner” and “Intended Removal” columns are critical. When an incident happens at 3 AM in a different time zone, the on-call engineer needs to know immediately who owns a flag and whether it’s safe to flip without escalating. A catalog with missing owners creates hesitation at exactly the wrong moment.
Gradual Rollout Pattern for Distributed Services
When releasing across multiple microservices with remote teams, coordinate flag rollout order to prevent version skew:
// Service A depends on Service B's new API
// Flag rollout order: enable in Service B first, then Service A
// service-b: new endpoint behind a flag
if (featureClient.isEnabled('payments-v2-api')) {
app.use('/api/v2/payments', newPaymentsRouter);
}
// service-a: only call new API if both services have flag enabled
// This prevents service-a from calling an endpoint that doesn't exist yet
const useV2 = featureClient.isEnabled('payments-v2-api') &&
featureClient.isEnabled('checkout-use-v2-payments');
const paymentsUrl = useV2
? 'https://payments.internal/api/v2/payments'
: 'https://payments.internal/api/v1/payments';
Sequencing flag rollouts across service boundaries is where remote teams most often introduce incidents — the service that consumes an API gets the flag before the service that provides it.
Clean Up Stale Flags
Flags accumulate. Run a weekly audit:
#!/bin/bash
# scripts/audit-flags.sh
# Check for flags not evaluated in the last 30 days (Unleash API)
UNLEASH_URL="https://flags.yourcompany.com"
TOKEN="$UNLEASH_API_TOKEN"
CUTOFF=$(date -d "30 days ago" --iso-8601)
curl -s \
-H "Authorization: $TOKEN" \
"$UNLEASH_URL/api/admin/features" \
| jq --arg cutoff "$CUTOFF" \
'.features[] | select(.lastSeenAt != null and .lastSeenAt < $cutoff) | {name, lastSeenAt, createdAt}'
Post the audit output to Slack automatically so stale flags don’t survive indefinitely:
# Append to crontab
# Run every Monday at 9 AM UTC
0 9 * * 1 /opt/scripts/audit-flags.sh | \
jq -Rs '{"text": "Weekly stale flag audit:\n```\(.)```"}' | \
curl -s -X POST -H "Content-Type: application/json" \
-d @- "$SLACK_WEBHOOK_URL"
Tool Comparison
| Tool | Hosting | Cost | Best For |
|---|---|---|---|
| Unleash | Self-hosted | Free (OSS) | Privacy-first, DevOps-mature teams |
| Flagsmith | Both | Free tier + paid | Flags + remote config together |
| LaunchDarkly | SaaS | From $12/seat/mo | Enterprise, experimentation |
| GrowthBook | Both | Free (OSS) | A/B testing focus |
| OpenFeature | N/A (SDK standard) | Free | Vendor portability |
For small remote teams (under 20 engineers), Flagsmith self-hosted covers most use cases and costs nothing. For teams scaling past 50 engineers with complex targeting requirements, LaunchDarkly’s operational maturity pays for itself in reduced incident time. OpenFeature is worth adopting regardless of which backend you choose — it protects your application code from vendor lock-in without adding meaningful overhead.
Related Reading
- How to Create Automated Canary Deployments
- Best Tools for Remote Team API Mocking
- How to Set Up Woodpecker CI for Self-Hosted
- Remote Team Feature Delivery Predictability Metric