Performance Optimization¶
Overview¶
This section covers performance characteristics of the Flaggy feature flag library and optimization techniques for high-throughput production environments.
Performance Architecture¶
Multi-Layer Caching Strategy¶
Flaggy implements a sophisticated multi-layer caching architecture:
- Application-Level Cache (MemoryCache or Redis)
- Configurable TTL (default: 5 minutes)
- Automatic cache invalidation on updates
-
Thread-safe operations
-
Provider-Level Storage
- Database-backed (MySQL, PostgreSQL, MS SQL)
- In-memory with JSON persistence
- Connection pooling support
// Cache flow: Service → Cache → Provider → Database
var flag = await _flagService.is_enabledAsync("my-flag");
// 1st call: Cache miss → Database read → Cache store
// 2nd call: Cache hit → Instant return (no DB query)
Performance Characteristics¶
| Operation | With Cache Hit | With Cache Miss | Notes |
|---|---|---|---|
is_enabledAsync() |
~0.1 ms | ~5-15 ms | Depends on DB latency |
GetValueAsync() |
~0.1 ms | ~5-15 ms | Includes type conversion |
GetAllFlagsAsync() |
~0.5 ms | ~10-30 ms | Bulk operation optimized |
| Cache Refresh | N/A | ~10-30 ms | Background operation |
CreateFlagAsync() |
~5-15 ms | ~5-15 ms | Writes to DB + cache |
Caching Strategies¶
Strategy 1: Memory Cache (Single Instance)¶
Best for single-server deployments or when consistency requirements are relaxed.
using Flaggy.Enums;
using Flaggy.Extensions;
builder.Services.AddFlaggy(
provider: new InMemoryFeatureFlagProvider(),
cachingProvider: CachingProvider.Memory,
cacheExpiration: TimeSpan.FromMinutes(5) // Tune based on needs
);
Pros: - Fast: In-process memory access (~100 μs) - No external dependencies - Low latency
Cons: - Not distributed: Each instance has its own cache - Cache inconsistency in multi-server setups - Memory usage scales with flag count
Best for: Single-instance apps, development, testing
Strategy 2: Redis Cache (Distributed)¶
Best for multi-server deployments requiring consistency across instances.
using Flaggy.Enums;
using Flaggy.Extensions;
builder.Services.AddFlaggy(
provider: new MySqlFeatureFlagProvider("..."),
cachingProvider: CachingProvider.Redis,
redisConnectionString: "localhost:6379,password=secret",
cacheExpiration: TimeSpan.FromMinutes(10)
);
Pros: - Distributed: Shared cache across all instances - Consistent: Flag changes visible to all servers - Pub/Sub support for real-time updates
Cons: - Network latency: ~1-2 ms per call - External dependency - Additional infrastructure
Best for: Multi-server production deployments, microservices
Strategy 3: Hybrid Local + Distributed Cache¶
Combine local memory cache with Redis for best performance and consistency.
public class HybridFeatureFlagCache : IFeatureFlagCache
{
private readonly IMemoryCache _localCache;
private readonly IFeatureFlagCache _distributedCache;
private readonly TimeSpan _localCacheExpiration = TimeSpan.FromSeconds(30);
public async Task<FeatureFlag?> GetAsync(string key, CancellationToken ct = default)
{
// L1: Check local cache first (fast)
if (_localCache.TryGetValue(key, out FeatureFlag? flag))
return flag;
// L2: Check distributed cache (slower but consistent)
flag = await _distributedCache.GetAsync(key, ct);
if (flag != null)
{
// Store in local cache for subsequent calls
_localCache.Set(key, flag, _localCacheExpiration);
}
return flag;
}
public async Task SetAsync(string key, FeatureFlag flag, CancellationToken ct = default)
{
// Update both caches
_localCache.Set(key, flag, _localCacheExpiration);
await _distributedCache.SetAsync(key, flag, ct);
}
public async Task RemoveAsync(string key, CancellationToken ct = default)
{
_localCache.Remove(key);
await _distributedCache.RemoveAsync(key, ct);
}
public async Task ClearAsync(CancellationToken ct = default)
{
_localCache.Clear();
await _distributedCache.ClearAsync(ct);
}
public Task<IEnumerable<FeatureFlag>> GetAllAsync(CancellationToken ct = default)
{
return _distributedCache.GetAllAsync(ct);
}
public Task SetAllAsync(IEnumerable<FeatureFlag> flags, CancellationToken ct = default)
{
return _distributedCache.SetAllAsync(flags, ct);
}
}
Cache Tuning¶
Optimal Cache Expiration Times¶
// Development: Short TTL for rapid iteration
builder.Services.AddFlaggy(
provider: new InMemoryFeatureFlagProvider(),
cacheExpiration: TimeSpan.FromSeconds(30)
);
// Production (low-change rate): Longer TTL for performance
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: connectionString
);
options.UseMemoryCache(TimeSpan.FromMinutes(15);
})
);
// Production (high-change rate): Shorter TTL for responsiveness
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: connectionString
);
options.UseMemoryCache(TimeSpan.FromMinutes(2);
})
);
Manual Cache Invalidation¶
For critical flags that need immediate propagation:
public class CriticalFlagService
{
private readonly IFeatureFlagService _flagService;
public async Task DisableFeatureImmediatelyAsync(string key)
{
await _flagService.DisableFlagAsync(key);
// Force cache refresh across all instances
await _flagService.RefreshCacheAsync();
}
}
Warm-Up Cache on Startup¶
Pre-load flags during application startup:
var app = builder.Build();
// Warm up cache before serving traffic
using (var scope = app.Services.CreateScope())
{
var flagService = scope.ServiceProvider.GetRequiredService<IFeatureFlagService>();
await flagService.GetAllFlagsAsync(); // Populates cache
}
app.Run();
Database Performance¶
Connection Pooling¶
All database providers use connection pooling by default:
// MySQL
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: "Server=localhost;Database=myapp;User=root;Password=pass;Pooling=true;MinPoolSize=5;MaxPoolSize=50;"
);
});
// PostgreSQL
builder.Services.AddFlaggy(options =>
{
options.UsePostgreSQL(
connectionString: "Host=localhost;Database=myapp;username=postgres;Password=pass;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
);
});
// MS SQL Server
builder.Services.AddFlaggy(options =>
{
options.UseMsSql(
connectionString: "Server=localhost;Database=myapp;User Id=sa;Password=pass;Pooling=true;Min Pool Size=5;Max Pool Size=50"
);
});
Index Optimization¶
Flaggy's auto-migration system creates optimal indexes:
-- MySQL
CREATE TABLE feature_flags (
`key` VARCHAR(255) PRIMARY KEY, -- Clustered index on Key
is_enabled BOOLEAN NOT NULL DEFAULT FALSE,
`value` TEXT NULL,
description TEXT NULL,
created_at DATETIME NULL,
updated_at DATETIME NULL
);
-- Additional index for queries filtering by is_enabled
CREATE INDEX idx_feature_flags_enabled ON feature_flags(is_enabled);
Query Optimization¶
// Bad - Multiple individual queries
foreach (var key in keys)
{
var flag = await flagService.GetFlagAsync(key);
// N+1 query problem
}
// Good - Single bulk query
var allFlags = await flagService.GetAllFlagsAsync();
var flagDict = allFlags.ToDictionary(f => f.Key);
foreach (var key in keys)
{
if (flagDict.TryGetValue(key, out var flag))
{
// Use flag
}
}
High-Throughput Patterns¶
Minimize Flag Checks in Hot Paths¶
// Bad - Checking flag on every request
app.MapGet("/api/products", async (IFeatureFlagService flagService) =>
{
var useCache = await flagService.is_enabledAsync("use-product-cache");
// Evaluated on every single request
});
// Good - Cache flag check result
public class ProductService
{
private readonly IFeatureFlagService _flagService;
private bool? _useCacheFlag;
private DateTime _flagLastChecked = DateTime.MinValue;
private readonly TimeSpan _recheckInterval = TimeSpan.FromSeconds(10);
public async Task<List<Product>> GetProductsAsync()
{
// Re-check flag every 10 seconds instead of every request
if (_useCacheFlag == null || DateTime.UtcNow - _flagLastChecked > _recheckInterval)
{
_useCacheFlag = await _flagService.is_enabledAsync("use-product-cache");
_flagLastChecked = DateTime.UtcNow;
}
return _useCacheFlag.Value
? await GetProductsFromCache()
: await GetProductsFromDatabase();
}
}
Lazy Flag Loading¶
public class LazyFeatureFlagService
{
private readonly IFeatureFlagService _flagService;
private readonly ConcurrentDictionary<string, Lazy<Task<bool>>> _lazyFlags = new();
public Task<bool> is_enabledAsync(string key)
{
var lazy = _lazyFlags.GetOrAdd(key, k => new Lazy<Task<bool>>(
() => _flagService.is_enabledAsync(k)
));
return lazy.Value;
}
public void Invalidate(string key)
{
_lazyFlags.TryRemove(key, out _);
}
}
Batch Flag Evaluation¶
public class BatchFlagService
{
private readonly IFeatureFlagService _flagService;
public async Task<Dictionary<string, bool>> GetMultipleFlagsAsync(params string[] keys)
{
var allFlags = await _flagService.GetAllFlagsAsync();
var flagDict = allFlags.ToDictionary(f => f.Key, f => f.is_enabled);
return keys.ToDictionary(
key => key,
key => flagDict.TryGetValue(key, out var enabled) && enabled
);
}
}
// Usage
var flags = await batchService.GetMultipleFlagsAsync(
"feature-1",
"feature-2",
"feature-3"
);
if (flags["feature-1"]) { /* ... */ }
if (flags["feature-2"]) { /* ... */ }
Memory Optimization¶
Minimize Flag Value Size¶
// Bad - Storing large JSON in flag value
await flagService.CreateFlagAsync(new FeatureFlag
{
Key = "config",
Value = JsonSerializer.Serialize(largeConfigObject) // Multiple KB
});
// Good - Store reference, fetch details from proper storage
await flagService.CreateFlagAsync(new FeatureFlag
{
Key = "config-version",
Value = "v2.1" // Small reference
});
// Fetch actual config from configuration system or database
Clean Up Unused Flags¶
public class FlagMaintenanceService
{
private readonly IFeatureFlagService _flagService;
private readonly ILogger<FlagMaintenanceService> _logger;
public async Task ArchiveOldFlagsAsync()
{
var flags = await _flagService.GetAllFlagsAsync();
var cutoffDate = DateTime.UtcNow.AddDays(-90);
var flagsToArchive = flags.Where(f =>
!f.is_enabled &&
f.updated_at.HasValue &&
f.updated_at.Value < cutoffDate
).ToList();
_logger.LogInformation("Archiving {Count} old flags", flagsToArchive.Count);
// Optional: Export to archive before deleting
await ExportToArchive(flagsToArchive);
// Delete from active system
foreach (var flag in flagsToArchive)
{
await _flagService.DeleteFlagAsync(flag.Key);
}
}
}
Monitoring and Metrics¶
Performance Monitoring¶
public class InstrumentedFeatureFlagService : IFeatureFlagService
{
private readonly IFeatureFlagService _inner;
private readonly IMetrics _metrics;
public async Task<bool> is_enabledAsync(string key, CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();
try
{
var result = await _inner.is_enabledAsync(key, ct);
_metrics.Histogram("feature_flag.check.duration_ms", sw.ElapsedMilliseconds);
_metrics.Increment("feature_flag.check.success");
return result;
}
catch (Exception ex)
{
_metrics.Increment("feature_flag.check.error");
throw;
}
}
// Implement other IFeatureFlagService methods...
}
Cache Hit Rate Monitoring¶
public class CacheMonitoringService
{
private long _cacheHits = 0;
private long _cacheMisses = 0;
public void RecordCacheHit() => Interlocked.Increment(ref _cacheHits);
public void RecordCacheMiss() => Interlocked.Increment(ref _cacheMisses);
public double GetHitRate()
{
var hits = _cacheHits;
var misses = _cacheMisses;
var total = hits + misses;
return total == 0 ? 0 : (double)hits / total * 100;
}
}
Benchmarks¶
Performance Comparison¶
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
public class FeatureFlagBenchmarks
{
private IFeatureFlagService _memoryCacheService;
private IFeatureFlagService _redisCacheService;
[GlobalSetup]
public void Setup()
{
// Setup services
}
[Benchmark]
public async Task<bool> MemoryCache_is_enabled()
{
return await _memoryCacheService.is_enabledAsync("test-flag");
}
[Benchmark]
public async Task<bool> RedisCache_is_enabled()
{
return await _redisCacheService.is_enabledAsync("test-flag");
}
[Benchmark]
public async Task<int?> GetIntValue_WithConversion()
{
return await _memoryCacheService.GetValueAsync<int>("test-flag", defaultValue: 0);
}
}
// Typical Results:
// MemoryCache_is_enabled: ~100 μs (cache hit)
// RedisCache_is_enabled: ~1-2 ms (network + cache hit)
// GetIntValue_WithConversion: ~120 μs (includes type conversion)
Production Configuration¶
Recommended Settings¶
// High-traffic production API
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: builder.Configuration.GetConnectionString("Flaggy"
);
}),
tableName: "feature_flags",
cacheExpiration: TimeSpan.FromMinutes(5), // Balance between freshness and performance
autoMigrate: false // Run migrations manually in production
);
// Configure Redis for distributed caching
builder.Services.AddFlaggy(
provider: sp.GetRequiredService<IFeatureFlagProvider>(),
cachingProvider: CachingProvider.Redis,
redisConnectionString: builder.Configuration["Redis:ConnectionString"],
cacheExpiration: TimeSpan.FromMinutes(5)
);
// Configure health checks
builder.Services.AddHealthChecks()
.AddCheck<FeatureFlagHealthCheck>("feature_flags");
Health Check Implementation¶
public class FeatureFlagHealthCheck : IHealthCheck
{
private readonly IFeatureFlagService _flagService;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken ct = default)
{
try
{
var sw = Stopwatch.StartNew();
await _flagService.GetAllFlagsAsync(ct);
sw.Stop();
if (sw.ElapsedMilliseconds > 1000)
{
return HealthCheckResult.Degraded(
$"Feature flag service is slow: {sw.ElapsedMilliseconds}ms"
);
}
return HealthCheckResult.Healthy(
$"Feature flag service is healthy ({sw.ElapsedMilliseconds}ms)"
);
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"Feature flag service is unavailable",
ex
);
}
}
}