Gradual Rollouts
Gradually release features from 0% to 100%
A gradual rollout releases a feature to a growing portion of users over time. Start at 0%, increase gradually, reach 100%.
How It Works
When you set a gradual rollout:
10% -> new-feature
90% -> old-featureFlipswitch:
- Hashes the user's
targetingKey - Maps the hash to a bucket (0-99)
- Returns the variant based on bucket
User alice@example.com might hash to bucket 42. With 10% rollout, she gets the old feature. Increase to 50%, and she gets the new feature.
Properties
Deterministic. Same user, same bucket, same variant. Alice always sees the same thing.
Consistent. Increasing from 10% to 20% doesn't reassign users in the first 10%. Users who had the new feature keep it.
Even distribution. Hash function distributes users evenly across buckets. 10% means roughly 10% of users, not exactly.
Implementation
1. Create the Flag
Key: new-pricing-page
Type: Boolean
Default: false (old page)2. Configure Rollout
Start at 0%:
0% -> true (new page)
100% -> false (old page)3. Evaluate in Code
const showNewPricing = await client.getBooleanValue('new-pricing-page', false, {
targetingKey: userId
});
return showNewPricing ? <NewPricingPage /> : <OldPricingPage />;MutableContext context = new MutableContext(userId);
boolean showNewPricing = client.getBooleanValue("new-pricing-page", false, context);
return showNewPricing ? newPricingPage() : oldPricingPage();context = EvaluationContext(targeting_key=user_id)
show_new_pricing = client.get_boolean_value("new-pricing-page", False, context)
return new_pricing_page() if show_new_pricing else old_pricing_page()evalCtx := openfeature.NewEvaluationContext(userID, nil)
showNewPricing, _ := client.BooleanValue(ctx, "new-pricing-page", false, evalCtx)
if showNewPricing {
return newPricingPage()
}
return oldPricingPage()4. Increase Gradually
Day 1: 1% -> true
Day 3: 5% -> true
Day 5: 10% -> true
Day 7: 25% -> true
Day 10: 50% -> true
Day 14: 100% -> trueWatch metrics at each stage before increasing.
The targetingKey
The targeting key determines which bucket a user falls into:
// Good: consistent per user
await client.getBooleanValue('feature', false, {
targetingKey: user.id // User always sees same variant
});
// Good: consistent per session (for anonymous users)
await client.getBooleanValue('feature', false, {
targetingKey: sessionId
});
// Bad: inconsistent
await client.getBooleanValue('feature', false, {
targetingKey: Math.random().toString() // Different every time!
});// Good: consistent per user
MutableContext context = new MutableContext(user.getId()); // User always sees same variant
client.getBooleanValue("feature", false, context);
// Good: consistent per session (for anonymous users)
MutableContext sessionContext = new MutableContext(sessionId);
client.getBooleanValue("feature", false, sessionContext);
// Bad: inconsistent
MutableContext badContext = new MutableContext(UUID.randomUUID().toString()); // Different every time!
client.getBooleanValue("feature", false, badContext);# Good: consistent per user
context = EvaluationContext(targeting_key=user.id) # User always sees same variant
client.get_boolean_value("feature", False, context)
# Good: consistent per session (for anonymous users)
session_context = EvaluationContext(targeting_key=session_id)
client.get_boolean_value("feature", False, session_context)
# Bad: inconsistent
import uuid
bad_context = EvaluationContext(targeting_key=str(uuid.uuid4())) # Different every time!
client.get_boolean_value("feature", False, bad_context)// Good: consistent per user
evalCtx := openfeature.NewEvaluationContext(user.ID, nil) // User always sees same variant
client.BooleanValue(ctx, "feature", false, evalCtx)
// Good: consistent per session (for anonymous users)
sessionCtx := openfeature.NewEvaluationContext(sessionID, nil)
client.BooleanValue(ctx, "feature", false, sessionCtx)
// Bad: inconsistent
badCtx := openfeature.NewEvaluationContext(uuid.New().String(), nil) // Different every time!
client.BooleanValue(ctx, "feature", false, badCtx)For stateless services, use a consistent identifier like request ID:
// Server-side with request correlation
await client.getBooleanValue('feature', false, {
targetingKey: request.headers['x-request-id']
});// Server-side with request correlation
MutableContext context = new MutableContext(request.getHeader("x-request-id"));
client.getBooleanValue("feature", false, context);# Server-side with request correlation
context = EvaluationContext(targeting_key=request.headers.get("x-request-id"))
client.get_boolean_value("feature", False, context)// Server-side with request correlation
evalCtx := openfeature.NewEvaluationContext(r.Header.Get("X-Request-ID"), nil)
client.BooleanValue(ctx, "feature", false, evalCtx)Combining with Rules
Targeting rules are evaluated before gradual rollouts. If a rule matches, the rollout is skipped:
Targeting Rules:
Rule 1: IF user in segment "internal" THEN return "enabled"
Rule 2: IF user in segment "beta" THEN return "enabled"
Gradual Rollout (for everyone else):
10% -> enabled
90% -> disabledInternal and beta users always get the new feature (via rules). Everyone else has a 10% chance (via rollout).
Multi-Variant Rollouts
For A/B tests or multiple variants:
Type: String
Variants: control, treatment-a, treatment-b
Rollout:
40% -> control
30% -> treatment-a
30% -> treatment-bconst variant = await client.getStringValue('checkout-experiment', 'control', {
targetingKey: userId
});MutableContext context = new MutableContext(userId);
String variant = client.getStringValue("checkout-experiment", "control", context);context = EvaluationContext(targeting_key=user_id)
variant = client.get_string_value("checkout-experiment", "control", context)evalCtx := openfeature.NewEvaluationContext(userID, nil)
variant, _ := client.StringValue(ctx, "checkout-experiment", "control", evalCtx)Ring-Based Rollouts
Some teams use "rings" for staged rollouts:
Ring 0: Internal employees (segment rule)
Ring 1: 1% of production
Ring 2: 10% of production
Ring 3: 50% of production
Ring 4: 100% of productionThis combines segments (for ring 0) with gradual rollouts (for rings 1-4).
Rollback
If something goes wrong at 25%:
Before: 25% -> new-feature
After: 0% -> new-featureAll users immediately get the old behavior. Users who had the new feature switch to the old one.
Cleanup
At 100% with stable metrics:
- Remove the flag check from code
- Keep the flag for a week (in case you need to roll back)
- Delete the flag
- Remove the old code path
// Before
if (showNewPricing) {
return <NewPricingPage />;
} else {
return <OldPricingPage />;
}
// After cleanup
return <NewPricingPage />;// Before
if (showNewPricing) {
return newPricingPage();
} else {
return oldPricingPage();
}
// After cleanup
return newPricingPage();# Before
if show_new_pricing:
return new_pricing_page()
else:
return old_pricing_page()
# After cleanup
return new_pricing_page()// Before
if showNewPricing {
return newPricingPage()
} else {
return oldPricingPage()
}
// After cleanup
return newPricingPage()