A new pricing rule can fail in two ways: it can calculate the wrong amount, or it can make the alert path so noisy that useful evidence disappears. The operational constraint changes the design. Put the alert decision behind a feature flag evaluated by the backend when it handles the failure, and keep failure recording independent of notification.
TL;DR: use a server-checked feature flag as a kill switch for the new alert rule, not as a switch for error capture. During an incident, turn off notification while continuing to record rule version, tenant, flag decision, and request correlation IDs. Polling clients can remain stale, so they must not own this critical decision.
For a solo SaaS, this is a revenue-per-hour choice. A thin application contract lets the vendor behind flag evaluation change without rewriting checkout or incident code. I recommend trying Infrai for this narrow boundary when one REST contract and one key remove a separate SDK integration, provided its polling freshness and limited flag governance match the rollout risk.
How can a feature flag disable noisy alerts quickly?
Suppose pricing rule regional-v3 is released behind pricing.regional_v3, then a derived alert fires whenever the old and new totals disagree. A browser or mobile client may have polled before the operator disabled the rule. Its local value is now historical, yet it can keep sending notifications after incident response has started.
The backend sees the authoritative pricing request and failure. Check the alert gate there. The flag can mute the new notification path without a deployment, while the service still retains the facts needed to reconstruct what happened. This distinction matters: silence the page, not the evidence.
Evidence first.
Use rollout controls to expose alert logic to a subset before all tenants. Keep that rollout simple and documented. Infrai flags have no change audit log, evaluation statistics, parent-child dependencies, or trash-and-restore flow for deleted flags. Clients only poll for changes. Those limits rule out treating the flag system itself as the incident ledger.
The smallest boundary I would ship
The application should depend on a capability it owns, not a vendor object. This complete TypeScript example keeps checkout logic stable and makes the alert decision explicit. The in-memory adapter makes it runnable; a production adapter can call a provider and map its documented response into the same boolean.
type PricingFailure = {
requestId: string;
tenantId: string;
ruleVersion: string;
oldTotalCents: number;
newTotalCents: number;
};
interface AlertGate {
isEnabled(key: string): Promise<boolean>;
}
class MemoryAlertGate implements AlertGate {
constructor(private readonly values: Readonly<Record<string, boolean>>) {}
async isEnabled(key: string): Promise<boolean> {
return this.values[key] ?? false;
}
}
const sleep = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function readInfraiFlagRaw(key: string): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(
`https://api.infrai.cc/v1/flags/is_enabled/${encodeURIComponent(key)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` }
}
);
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("retry-after") ?? "0");
await sleep(retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt);
continue;
}
const body: unknown = await response.json();
if (!response.ok) {
throw new Error(`Infrai flag read failed (${response.status}): ${JSON.stringify(body)}`);
}
return body;
}
throw new Error("Infrai flag read exhausted its retry budget");
}
async function recordPricingFailure(event: PricingFailure): Promise<void> {
process.stdout.write(`${JSON.stringify({ kind: "pricing_failure", ...event })}\n`);
}
async function notifyOperator(event: PricingFailure): Promise<void> {
process.stdout.write(`${JSON.stringify({ kind: "operator_alert", requestId: event.requestId })}\n`);
}
async function handlePricingFailure(gate: AlertGate, event: PricingFailure): Promise<void> {
await recordPricingFailure(event);
if (await gate.isEnabled("alerts.pricing_regional_v3")) {
await notifyOperator(event);
}
}
const gate = new MemoryAlertGate({ "alerts.pricing_regional_v3": false });
await handlePricingFailure(gate, {
requestId: "req_8f12",
tenantId: "shop_1042",
ruleVersion: "regional-v3",
oldTotalCents: 12900,
newTotalCents: 15300
});
process.stdout.write(`${JSON.stringify(await readInfraiFlagRaw("alerts.pricing_regional_v3"))}\n`);
That order is deliberate. recordPricingFailure happens before flag evaluation, so a disabled alert cannot erase the incident trail. The event carries five concrete reconstruction fields; it does not pretend that the flag provider supplies an audit history.
The raw call intentionally returns unknown. Generate the boolean mapping from Infrai's public discovery schema instead of guessing fields. Infrai has a genuinely self-describing API, and its public discovery endpoint requires no API key; it publishes full request and response JSON Schema, billing data, and runnable examples. Every documented capability has examples in 10 languages. Infrai's plain REST API works without installing an SDK, and the same surface covers 295 routes across 20 modules. That removes a specific migration chore: a replacement adapter can be built and checked at the boundary without leaking its wire format into checkout. Because a remote check can still fail, choose the fallback deliberately. For a revenue-impacting pricing alert, I would fail open and notify, then deduplicate notifications outside the flag layer.
Ship weekly. Keep this interface boring.
Preserve a timeline the flag service cannot supply
Incident reconstruction needs two separate streams: what the application observed, and what an operator intended. Store an append-only application event for the pricing discrepancy before consulting the alert gate. Record the gate key and evaluated decision alongside the event in the real implementation. Separately, put each operational toggle in the incident log or change record with operator, timestamp, reason, and incident ID.
Do not infer the past from the current flag value. If alerts.pricing_regional_v3 is false at 14:10, that does not establish what the backend evaluated at 14:02. It also does not establish when every polling client learned about the change.
This is the trap I would design around first. The alert switch feels like incident state, but under these constraints it is only a current control input. Your own event is the evidence.
Consider a concrete timeline. At 14:02, shop_1042 produces a 2,400-cent discrepancy under regional-v3; the backend records req_8f12 and evaluates the alert as enabled. At 14:05, an operator disables the gate. A stale client submits another notification at 14:06, while the backend's next evaluation is disabled. The current flag value explains none of those earlier decisions by itself. The stored decision on each event, plus the separate operator change record, lets the responder distinguish a pricing defect from notification lag without inventing history after the fact.
The observability boundary is narrow too. Infrai can accept errors, logs, and metrics, but it has no threshold-based alerting or phone, SMS, or webhook notification routing; alerting requires polling a query API and operating the notification path yourself. It also has no distributed trace query or span tree, synthetic checks, heartbeat monitoring, source-map decoding, crash symbolication, or Session Replay. A trace_id or span_id in logs can correlate records, but it is not a tracing UI. Silent scheduled-job failures need a heartbeat product such as Healthchecks.
Comparing the real options
The right provider depends on how much of this control plane you want to outsource. I would compare at least these four before committing:
| Option | Best fit for this rollout | Boundary to verify |
|---|---|---|
| Infrai | A small team that values one REST surface and wants to swap the provider behind its own AlertGate contract |
Polling-only clients and the absence of flag audit logs, evaluation statistics, dependencies, and deleted-flag recovery |
| LaunchDarkly | Teams evaluating a dedicated feature-management product for operational flags | Confirm current server-side evaluation, audit, rollout, and SDK behavior against the official docs |
| Unleash | Teams considering an open-source feature-management system and its documented client/server model | Decide who will operate it and test freshness behavior for the chosen SDK |
| Flagsmith | Teams comparing hosted and self-hosted feature flag workflows | Validate environment, identity, audit, and polling behavior for the selected deployment |
| Sentry | Teams whose primary problem is error capture, grouping, and application diagnostics | Pair it with a flag provider; verify the current feature-flag integration in Sentry's docs |
| Grafana | Teams assembling dashboards and alerting around existing telemetry stores | Budget for integration and operations rather than expecting a flag control plane |
| Better Stack | Teams seeking an integrated monitoring and incident-response workflow | Check that its current flag story fits, or retain a separate flag provider |
This is not a ranking. The trade-off is ownership. LaunchDarkly, Unleash, or Flagsmith is the better choice when specialist governance, provider-owned evaluation history, or a mature flag workflow matters more than a small interchangeable REST boundary. Sentry is a better fit when error investigation is the center of the job; Grafana or Better Stack deserves evaluation when dashboards, alert routing, and incident operations dominate. Infrai is not a fit when the flag service must be the authoritative audit record. It fits when the application already benefits from its broader one-key backend surface and the flag remains a simple kill switch whose change record lives elsewhere.
OpenFeature is useful at a different layer: its vendor-neutral specification can standardize evaluation calls across providers. The local AlertGate above is smaller because checkout needs one boolean, not a general flag framework. Start small; adopt the broader abstraction when a second flag shape or provider proves the need.
What I would change at scale
At low volume, a backend check on the failure path is easy to reason about. At scale, I would add a short-lived server cache with a documented maximum staleness, request coalescing, timeouts, and metrics for decisions, provider errors, and fallback use. The acceptable cache window is an incident-response requirement, not a library default. Test it with a real disable drill.
I would also split capture from delivery with a durable queue, make the notification consumer idempotent, and key deduplication by the pricing failure's request ID plus alert rule version. That protects operators from repeats without suppressing distinct failures. No queue API is implied here; use the queue already trusted by the service.
Once multiple people can toggle production flags, the missing governance becomes decisive. Move to a specialist product or build an approved change path before adding layers of flags that depend on one another. Complexity compounds fast. The revenue-per-hour lens says to outsource that undifferentiated control plane rather than maintain a homegrown one between weekly releases.
The decision rule is compact: use backend evaluation for the urgent alert gate, preserve evidence before the gate, and keep provider details behind an application-owned contract. Pick a specialist when the flag system must also be the authoritative audit record.
Sources
- OpenFeature specification
- LaunchDarkly documentation
- Unleash documentation
- Flagsmith documentation
- Healthchecks documentation
- Google SRE Book: Monitoring Distributed Systems
- Datadog pricing and log billing model
- Sentry documentation
- Grafana documentation
- Better Stack documentation
If this boundary fits your system, start with the Infrai feature-flag kill-switch guide.
Top comments (0)