Troubleshooting
Practical problem/solution guide for Pragmatic.FeatureFlags. Each section covers a common issue, the likely causes, and the fix.
Flag Always Returns False
Section titled “Flag Always Returns False”A flag is defined but IsEnabledAsync always returns false.
Checklist
Section titled “Checklist”-
Is the flag defined?
GetDefinitionAsyncreturnsnullfor unknown flags, andIsEnabledAsyncreturnsfalsefor unknown flags. Verify the flag exists in the store:var definition = await store.GetDefinitionAsync("new-checkout");if (definition is null)Console.WriteLine("Flag not defined in the store"); -
Is the flag name spelled correctly? Lookup is case-insensitive, but typos are not caught.
"new-checkout"and"new_checkout"are different flags. UseIFeatureFlagtypes for compile-time safety. -
Is the global
Enabledstatefalse? If no rules match, the globalEnabledis the fallback. A flag withEnabled = falseand no matching rules always returnsfalse. -
Are rules matching the context? Rules require the corresponding context property to be non-null:
tenantrule requirescontext.TenantIduserrule requirescontext.UserIdplanrule requirescontext.Planenvironmentrule requirescontext.Environmentpropertyrule requires the key incontext.Properties
If the context property is null, the rule is skipped (does not match).
-
Is a deny rule matching before an allow rule? Rules are first-match-wins. Check that a deny rule (with
Enabled = false) is not matching before your allow rule. -
For percentage rules: Is the percentage greater than 0? A value of
"0"always returns false. Also verify the user/tenant ID produces a bucket within the target range — use a different test user if needed.
Flag Always Returns True
Section titled “Flag Always Returns True”A flag is enabled for everyone, regardless of targeting rules.
Checklist
Section titled “Checklist”-
Is the global
Enabledset totruewith no deny rules? IfEnabled = trueand no rules match, the fallback istrue. Add specific deny rules before allow rules if needed. -
Is a broad allow rule matching before specific deny rules? Remember: first match wins. Reorder rules so deny rules come first:
Rules =[new FeatureFlagRule { Type = "tenant", Values = ["blocked-tenant"], Enabled = false }, // firstnew FeatureFlagRule { Type = "percentage", Values = ["50"], Enabled = true }, // second] -
Is the percentage set to 100? A percentage rule with
Values = ["100"]always matches.
Percentage Rollout Not Working as Expected
Section titled “Percentage Rollout Not Working as Expected”The percentage rollout does not seem to match the expected distribution.
Checklist
Section titled “Checklist”-
Small sample size. Deterministic hashing guarantees the same result per user, but with a small number of users, the distribution may not be uniform. Test with at least 100 distinct user IDs.
-
Same user on different flags. The hash seed includes the flag name. User “user-42” may be in the 10% bucket for
flag-abut not forflag-b. This is by design — different flags have independent distributions. -
No UserId or TenantId in context. If both are null,
"anonymous"is used as the seed. All requests without identity get the same bucket — the percentage becomes all-or-nothing, not per-user. -
Percentage value parsing. The value must be a valid integer string:
["20"]. Non-numeric values cause the rule to be skipped.
WatchAsync Not Emitting Changes
Section titled “WatchAsync Not Emitting Changes”WatchAsync does not emit FeatureFlagChange events.
Checklist
Section titled “Checklist”-
Are you changing the global
Enabledstate? InInMemoryFeatureFlagStore, only changes to the globalEnabledproperty trigger a notification. Adding or modifying rules without changingEnableddoes not emit a change. -
Is the flag being defined for the first time? The initial
Define()does not emit a change (there is no “previous” state to compare against). -
Is the consumer reading from
WatchAsyncbefore the change? TheChannel<T>buffers changes, but if you start consuming after the change, you will see it. If you start consuming, then restart, buffered changes may be lost. -
Custom store implementation. Custom
IFeatureFlagStoreimplementations control their own notification semantics. The store may use polling, database notifications, or webhooks. Check the store documentation.
DI Registration Conflicts
Section titled “DI Registration Conflicts”The wrong store implementation is resolved, or flags are not available.
Checklist
Section titled “Checklist”-
AddPragmaticFeatureFlags()usesTryAdd. If you register a custom store after callingAddPragmaticFeatureFlags(), your store is ignored (the in-memory store was registered first). Register your store before or use the generic overload:// Option 1: register beforeservices.AddSingleton<IFeatureFlagStore, MyDatabaseStore>();services.AddPragmaticFeatureFlags(); // TryAdd won't replace// Option 2: generic overload (always replaces)services.AddPragmaticFeatureFlags<MyDatabaseStore>(); -
SG auto-registration. The SG registers
AddPragmaticFeatureFlags()when it detects the package. If you also call it manually, theTryAddin the default overload ensures no conflict. If you use the generic overload, it replaces the SG-registered store. -
Store lifetime. The default registration is singleton. If your custom store needs scoped services (e.g., a scoped
DbContext), register it as scoped — but be aware that singleton consumers cannot inject a scoped store. UseIServiceScopeFactoryin that case.
Property Rules Not Matching
Section titled “Property Rules Not Matching”A property rule is defined but never matches.
Checklist
Section titled “Checklist”-
Check the Values format. For property rules,
Values[0]is the property key andValues[1..]are the allowed values. At least 2 values are required (key + at least one allowed value):// Correct: key="region", allowed=["eu-west", "eu-east"]new FeatureFlagRule { Type = "property", Values = ["region", "eu-west", "eu-east"] }// Wrong: only 1 value -- interpreted as key with no allowed values, rule skippednew FeatureFlagRule { Type = "property", Values = ["region"] } -
Is the property in the context? The property key must exist in
context.Properties:var context = new FeatureFlagContext{Properties = new Dictionary<string, string>{["region"] = "eu-west" // must match Values[0]}}; -
Case sensitivity of property keys. The property key lookup uses
TryGetValueon the dictionary, which respects the dictionary’s comparer. The defaultDictionary<string, string>is case-sensitive. If your key is"Region"in the dictionary but"region"in the rule values, it will not match.The property value comparison is case-insensitive (handled by the evaluator), but the property key lookup depends on the dictionary comparer.
Frequently Asked Questions
Section titled “Frequently Asked Questions”Can I change a flag at runtime without redeploying?
Section titled “Can I change a flag at runtime without redeploying?”Yes. That is the primary purpose of feature flags. With InMemoryFeatureFlagStore, call Define() to update a flag. With a database store, update the database record. The store implementation determines how changes propagate.
Can I use feature flags without Pragmatic.MultiTenancy?
Section titled “Can I use feature flags without Pragmatic.MultiTenancy?”Yes. Feature flags have no dependency on multi-tenancy. Set context.TenantId to null (or use FeatureFlagContext.Empty) if you do not use multi-tenancy. Tenant-targeting rules will simply be skipped.
How do I test feature flags in unit tests?
Section titled “How do I test feature flags in unit tests?”Use InMemoryFeatureFlagStore directly:
var store = new InMemoryFeatureFlagStore();store.Define(new FeatureFlagDefinition { Name = "my-flag", Enabled = true });
var result = await store.IsEnabledAsync("my-flag");result.Should().BeTrue();How do I list all defined flags?
Section titled “How do I list all defined flags?”var allFlags = await store.GetAllAsync(ct);// Returns IReadOnlyList<FeatureFlagDefinition> ordered by nameIs FeatureFlagEvaluator usable from custom stores?
Section titled “Is FeatureFlagEvaluator usable from custom stores?”No. FeatureFlagEvaluator is internal to the Pragmatic.FeatureFlags package. Custom stores must implement their own evaluation logic or load definitions and process rules manually. The evaluation algorithm is documented in Concepts for reference.
What happens with concurrent flag updates?
Section titled “What happens with concurrent flag updates?”InMemoryFeatureFlagStore uses ConcurrentDictionary, so concurrent Define() calls are thread-safe. The last write wins for the flag definition. Change notifications are emitted per Define() call.
Getting Help
Section titled “Getting Help”- Verify flag definitions. Use
GetDefinitionAsyncandGetAllAsyncto inspect the current state of flags in the store. - Inspect the context. Log the
FeatureFlagContextbefore evaluation to verify all targeting dimensions are populated. - Test with the evaluator. Write unit tests against
InMemoryFeatureFlagStoreto verify rule behavior in isolation. - Read the guides. See Concepts, Getting Started, Stores, and Common Mistakes.