Implement Server-Side Feature Flags
Why implement server-side flags
Section titled “Why implement server-side flags”Client-side flags execute in the browser, where they are visible, slow to load, and easy to circumvent. Server-side flags evaluate on your infrastructure, keeping business logic hidden and decisions fast. Use them for backend features, pricing rules, API behavior changes, and any logic that should not be exposed to end users.
Step 1: Install the SDK
Section titled “Step 1: Install the SDK”npm install @optimizely/optimizely-sdk pip install optimizely-sdk dotnet add package Optimizely.SDK Step 2: Initialize the client
Section titled “Step 2: Initialize the client”Create a single SDK instance at application startup. The SDK downloads the datafile (your project configuration) and keeps it in memory for fast evaluations. Never create a new instance per request.
import { createInstance } from '@optimizely/optimizely-sdk';
// Create once at application startup
const optimizely = createInstance({
sdkKey: process.env.OPTIMIZELY_SDK_KEY,
datafileOptions: {
autoUpdate: true,
updateInterval: 60_000, // Poll for changes every 60 seconds
},
});
// Wait for the datafile to download before serving traffic
await optimizely.onReady();
console.log('Optimizely SDK initialized'); import os
from optimizely import optimizely
from optimizely.config_manager import PollingConfigManager
# Create once at application startup
config_manager = PollingConfigManager(
sdk_key=os.environ['OPTIMIZELY_SDK_KEY'],
update_interval=60, # Poll for changes every 60 seconds
)
client = optimizely.Optimizely(config_manager=config_manager)
print('Optimizely SDK initialized') using OptimizelySDK;
// In Startup.cs or Program.cs — create once at application startup
var optimizely = OptimizelyFactory.NewDefaultInstance(
Environment.GetEnvironmentVariable("OPTIMIZELY_SDK_KEY")
);
// Register as a singleton in your DI container
builder.Services.AddSingleton(optimizely); Key points:
- One instance per application — The SDK caches the datafile in memory. Multiple instances waste resources and can cause inconsistent evaluations.
- Auto-update — Enable polling so flag changes propagate without redeployment.
- SDK key — Found in the Optimizely application under Settings → Environments. Use the correct key for each environment (development, staging, production).
Step 3: Create user contexts
Section titled “Step 3: Create user contexts”A user context represents the person (or entity) making a request. Pass a unique identifier and any attributes needed for audience targeting.
// In your request handler (Express, Fastify, etc.)
app.get('/api/checkout', (req, res) => {
const user = optimizely.createUserContext(req.userId, {
country: req.geoCountry,
plan: req.user.plan,
device_type: req.headers['x-device-type'] || 'desktop',
is_employee: req.user.isEmployee,
});
const decision = user.decide('new_checkout_flow');
// ... handle decision
}); # In your request handler (Flask, Django, FastAPI, etc.)
@app.route('/api/checkout')
def checkout(request):
user = client.create_user_context(request.user_id, {
'country': request.geo_country,
'plan': request.user.plan,
'device_type': request.headers.get('X-Device-Type', 'desktop'),
'is_employee': request.user.is_employee,
})
decision = user.decide('new_checkout_flow')
# ... handle decision // In your controller or request handler
[HttpGet("/api/checkout")]
public IActionResult Checkout()
{
var user = _optimizely.CreateUserContext(HttpContext.User.GetUserId(), new UserAttributes
{
{ "country", Request.Headers["X-Geo-Country"].ToString() },
{ "plan", HttpContext.User.GetPlan() },
{ "device_type", Request.Headers["X-Device-Type"].ToString() },
{ "is_employee", HttpContext.User.IsEmployee() },
});
var decision = user.Decide("new_checkout_flow");
// ... handle decision
} User ID guidance:
- Use a stable identifier — logged-in user ID, account ID, or device ID
- The same user ID always gets the same variation (sticky bucketing)
- For anonymous users, generate a UUID and store it in a cookie or session
Step 4: Evaluate flags
Section titled “Step 4: Evaluate flags”The decide method returns a decision object containing the flag state, variation key, and variable values.
const decision = user.decide('search_algorithm');
if (decision.enabled) {
// Flag is on for this user
const algorithm = decision.variables.algorithm_name; // e.g., 'semantic_v2'
const maxResults = decision.variables.max_results; // e.g., 50
const results = search(query, { algorithm, maxResults });
} else {
// Flag is off — use defaults
const results = search(query, { algorithm: 'keyword_v1', maxResults: 20 });
}
// Log which variation was served (for debugging)
console.log(`Flag: search_algorithm | Variation: ${decision.variationKey}`); decision = user.decide('search_algorithm')
if decision.enabled:
# Flag is on for this user
algorithm = decision.variables['algorithm_name'] # e.g., 'semantic_v2'
max_results = decision.variables['max_results'] # e.g., 50
results = search(query, algorithm=algorithm, max_results=max_results)
else:
# Flag is off — use defaults
results = search(query, algorithm='keyword_v1', max_results=20)
# Log which variation was served (for debugging)
print(f'Flag: search_algorithm | Variation: {decision.variation_key}') var decision = user.Decide("search_algorithm");
if (decision.Enabled)
{
// Flag is on for this user
var algorithm = decision.Variables.GetValue<string>("algorithm_name");
var maxResults = decision.Variables.GetValue<int>("max_results");
var results = _searchService.Search(query, algorithm, maxResults);
}
else
{
// Flag is off — use defaults
var results = _searchService.Search(query, "keyword_v1", 20);
}
// Log which variation was served (for debugging)
_logger.LogInformation("Flag: search_algorithm | Variation: {Variation}", decision.VariationKey); Step 5: Track events
Section titled “Step 5: Track events”Send conversion events when users complete meaningful actions. Events are matched to running experiments for results analysis.
// After a successful search interaction
user.trackEvent('search_click');
// After a purchase — include revenue in cents
user.trackEvent('purchase', {
revenue: 2499,
quantity: 1,
}); # After a successful search interaction
user.track_event('search_click')
# After a purchase — include revenue in cents
user.track_event('purchase', {
'revenue': 2499,
'quantity': 1,
}) // After a successful search interaction
user.TrackEvent("search_click");
// After a purchase — include revenue in cents
user.TrackEvent("purchase", new EventTags
{
{ "revenue", 2499 },
{ "quantity", 1 },
}); Error handling and resilience
Section titled “Error handling and resilience”Flag evaluations should never break your application. The SDK is designed to fail gracefully — if the datafile is unavailable or an error occurs, decide returns a decision with enabled = false. Build your code to handle this.
function getSearchConfig(user) {
try {
const decision = user.decide('search_algorithm');
if (decision.enabled && decision.variables.algorithm_name) {
return {
algorithm: decision.variables.algorithm_name,
maxResults: decision.variables.max_results ?? 20,
};
}
} catch (error) {
console.error('Flag evaluation failed:', error);
}
// Default values — always have a fallback
return { algorithm: 'keyword_v1', maxResults: 20 };
} def get_search_config(user):
try:
decision = user.decide('search_algorithm')
if decision.enabled and decision.variables.get('algorithm_name'):
return {
'algorithm': decision.variables['algorithm_name'],
'max_results': decision.variables.get('max_results', 20),
}
except Exception as e:
print(f'Flag evaluation failed: {e}')
# Default values — always have a fallback
return {'algorithm': 'keyword_v1', 'max_results': 20} public SearchConfig GetSearchConfig(OptimizelyUserContext user)
{
try
{
var decision = user.Decide("search_algorithm");
if (decision.Enabled)
{
return new SearchConfig
{
Algorithm = decision.Variables.GetValue<string>("algorithm_name") ?? "keyword_v1",
MaxResults = decision.Variables.GetValue<int?>("max_results") ?? 20,
};
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Flag evaluation failed");
}
// Default values — always have a fallback
return new SearchConfig { Algorithm = "keyword_v1", MaxResults = 20 };
} Performance considerations
Section titled “Performance considerations”- Evaluations are local — The SDK evaluates flags in memory (microseconds). No network calls occur at decision time.
- Datafile polling is background — Updates download asynchronously without blocking requests.
- Batch events — The SDK batches track calls and sends them in bulk to reduce network overhead.
- Minimize attributes — Only pass attributes you actually use in audience conditions. Passing dozens of unused attributes wastes memory.
Troubleshooting
Section titled “Troubleshooting”| Issue | Cause | Fix |
|---|---|---|
decide always returns enabled: false | SDK not initialized or wrong SDK key | Verify the SDK key and check onReady() resolved |
| Same user gets different variations | Different user IDs across requests | Use a stable, consistent user ID |
| Flag changes not reflected | Datafile not polling | Confirm autoUpdate is enabled and the interval is reasonable |
| Events not appearing in results | Event name mismatch or event batching delay | Verify event names match and wait 5-10 minutes for batched events |
| High memory usage | Creating new SDK instances per request | Use a single shared instance (singleton pattern) |