DEV Community

VelvetDusk629047
VelvetDusk629047

Posted on

Feature Flag Percentage Rollouts: Canary Releasing New SaaS Pricing Rules Safely

Short answer: Use a deterministic percentage rollout for a basic canary release, watch application health independently, and increase exposure only when the new pricing rule stays inside explicit error and support thresholds.

The decision turns on signal quality. A five-percent canary is useful only when the same SaaS user sees the same rule on every request and the team can separate failures in the new cohort from background noise. Start low. Keep an immediate off switch.

Decision table for a pricing-rule canary

Option Pick it when Main trade-off
A small deterministic rollout in the backend The release is a simple on/off pricing rule and deployment ownership is clear The team owns configuration, monitoring, and auditability
Infrai A plain REST API and a self-describing integration are more useful than a full experimentation suite Flag evaluation has no statistics or dependency rules; clients poll
GrowthBook The canary is becoming an A/B experiment that needs an experimentation platform More experimental machinery than a basic release decision needs
LaunchDarkly A dedicated feature-management product is already on the evaluation shortlist Validate its current operating model against your governance and backend requirements
Unleash An open-source feature-management option belongs in the build-versus-buy review Operating and governance requirements need a separate product evaluation
Datadog, Grafana, or Sentry beside a flag service The missing piece is rollout health evidence rather than flag allocation These are observability candidates, so the flag control path remains separate

These are not interchangeable purchases. A rollout answers, “Who gets the rule?” An experiment also asks, “Did the rule cause the measured change?” Treating the first question as proof of the second produces confident charts and weak decisions.

For the fintech scenario here, the flag controls a new pricing rule for existing SaaS accounts. The first release goal is operational: expose a stable cohort, detect harm, and roll back. It is not to infer causality about conversion or revenue.

When each option earns a place

The backend-only approach is the narrowest. Pick it when one service owns the price calculation, a reviewed configuration path already exists, and operators can disable the rule without shipping new code. The catch is that the “tiny helper” becomes infrastructure as soon as several services, regions, or teams must coordinate it. Audit history and access control then matter as much as the hash function.

Infrai fits the same basic-canary lane when the team wants the control surface behind plain HTTP. Its public discovery surface describes each capability with the real method, path, request and response schemas, billing details, and runnable examples, so wiring the flag begins by reading the capability rather than installing and learning another SDK. Infrai puts 295 routes across 20 modules behind a single API key and a single bill. In this workflow, that one credential can authorize the flag control plus metrics or error polling; the team avoids adding another SDK, secret, and billing relationship for each backend capability. The honest boundary is important: its flags do not provide evaluation statistics, parent-child dependencies, or a change audit log, and client evaluation is polling-based. Alert and notification routing is not built in either, so application metrics or error data must be polled by your own alerting process.

GrowthBook is the clearer candidate when “canary” really means an A/B test. It is an open-source feature flag and experimentation platform, which puts evaluation in scope rather than leaving it as a homegrown analysis job. That extra scope is useful only if someone owns hypotheses, exposure definitions, and the resulting decisions.

LaunchDarkly and Unleash are credible names for a dedicated feature-management shortlist. This article does not have enough verified, like-for-like detail to rank their current capabilities against each other, and I’m not sure a generic ranking would help a team with specific residency, governance, or hosting constraints anyway. Use their current documentation and a proof of concept for that evaluation. Do not turn brand recognition into an architecture decision.

The observability side deserves its own shortlist. Evaluate Datadog or Grafana when metrics, dashboards, and alert ownership are the open decision; evaluate Sentry when error investigation is the gap. Keep that selection separate from cohort allocation. A strong alerting tool does not decide which account receives a price, and a flag service without notification routing does not page the operator.

How should a Node.js backend API roll out a feature flag by percentage?

Use a stable subject identifier, a fixed salt for the rule version, and a deterministic hash. Never call Math.random() during request handling. Random selection can move the same account between old and new pricing on consecutive requests, which is noisy for telemetry and unacceptable for a customer-facing quote.

Here is a complete TypeScript example. It reads the flag document from Infrai’s verified control-plane route without assuming undocumented response fields, then applies a deterministic local cohort for the pricing decision. The prices are illustrative test values, not a recommendation for a real product. Save it as rollout.ts and run it with a TypeScript-capable Node.js setup.

import { createHash } from "node:crypto";

type Rollout = {
  enabled: boolean;
  percentage: number;
  salt: string;
};

type Quote = {
  accountId: string;
  amountCents: number;
  pricingRule: "current" | "candidate";
};

function wait(milliseconds: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);

    const dateDelay = Date.parse(retryAfter) - Date.now();
    if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
  }

  return 500 * 2 ** attempt;
}

async function getFlagDocument(flagKey: string): Promise<unknown> {
  const apiKey = process.env.INFRAI_API_KEY;
  const apiBase = process.env.INFRAI_API_BASE_URL;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");
  if (!apiBase) throw new Error("INFRAI_API_BASE_URL is required");

  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(
      `${apiBase}/flags/get/${encodeURIComponent(flagKey)}`,
      {
        method: "GET",
        headers: { Authorization: `Bearer ${apiKey}` },
      },
    );

    if (response.status === 429 && attempt < 4) {
      await wait(retryDelay(response, attempt));
      continue;
    }

    const body = await response.text();
    if (!response.ok) {
      throw new Error(`Flag request returned ${response.status}: ${body}`);
    }

    return body ? JSON.parse(body) as unknown : null;
  }

  throw new Error("Flag request exhausted its retry budget");
}

function isExposed(accountId: string, rollout: Rollout): boolean {
  if (!Number.isFinite(rollout.percentage) ||
      rollout.percentage < 0 ||
      rollout.percentage > 100) {
    throw new RangeError("percentage must be between 0 and 100");
  }

  if (!rollout.enabled) return false;

  const digest = createHash("sha256")
    .update(`${rollout.salt}:${accountId}`)
    .digest();
  const bucket = digest.readUInt32BE(0) / 2 ** 32;

  return bucket < rollout.percentage / 100;
}

function quote(accountId: string, rollout: Rollout): Quote {
  const candidate = isExposed(accountId, rollout);

  return {
    accountId,
    amountCents: candidate ? 2_900 : 2_500,
    pricingRule: candidate ? "candidate" : "current",
  };
}

const rollout: Rollout = {
  enabled: true,
  percentage: 5,
  salt: "pricing-rule-2026-08",
};

const flagDocument = await getFlagDocument("pricing-rule");
console.log({ flagDocument });

for (const accountId of ["acct_1042", "acct_2088", "acct_9017"]) {
  console.log(quote(accountId, rollout));
}
Enter fullscreen mode Exit fullscreen mode

Diagram in words: account ID plus version salt goes into SHA-256; the first 32 bits become a number from zero up to, but not including, one; the rollout percentage draws a line across that range; accounts below the line receive the candidate rule. The boundary moves upward as exposure increases, so a user admitted at five percent remains admitted at ten percent. Changing the salt intentionally reshuffles the cohort, which is why it should change only with a new rule version.

Use the account ID, not a request ID and not an individual session ID. Pricing is normally an account-level promise. If two employees from the same customer can receive different contractual prices, the flag has chosen the wrong unit.

There is another sharp edge. A percentage describes allocation, not traffic. Five percent of accounts may generate 40 percent of quote requests if a few large customers dominate usage. Before raising the percentage, compare both unique exposed accounts and request volume; otherwise the nominally small canary can carry a surprisingly large operational blast radius.

Signal quality, step size, and rollback

Instrument the decision at the point where the backend selects the rule. Each pricing outcome should carry the flag key, rule version, chosen variant, and stable account identifier into logs or metrics, subject to the company’s privacy policy. Then compare the candidate and current cohorts on application health: quote error rate, latency, rejected payments, and support contacts tied to confusing prices. Revenue movement may be interesting, but it is not an early health signal.

Define gates before exposure begins. For example, a team can decide that it will pause after each step for one normal business cycle and roll back if the candidate’s error-rate delta crosses its agreed threshold. The actual threshold and observation window depend on baseline volume and risk; no universal number is justified here. Low traffic may require a longer wait. A launch during an unusual billing day may require a cleaner baseline. Your mileage may vary.

Noise wins when teams watch too many panels. Choose a small set of release-blocking signals, assign an owner, and separate them from diagnostics. The blocking set answers one question: “Is it safe to continue?” Detailed logs answer the later question: “Why did this account fail?” Mixing those views encourages someone to rationalize a red health signal because six unrelated charts still look green.

Increase exposure in explicit steps, such as low, medium, and broad, rather than continuously turning a dial. The exact percentages are a release decision, not a law. At every step, record who approved the change, the observed signals, and the rollback condition outside the flag system if the chosen system has no audit log.

Rollback must be boring.

Turning the flag off should route every subsequent evaluation to the current pricing rule without a deployment. Keep the old calculation available until the canary is complete and downstream effects are understood. A flag cannot reverse invoices already issued or messages already sent, so any side effect needs its own reconciliation plan and idempotent operation design.

Limits that change the choice

A basic percentage rollout is not suitable when the team needs statistically sound experiment results, evaluation telemetry supplied by the flag service, dependency rules between flags, or a durable change audit. Stick with an experimentation platform such as GrowthBook for causal product questions, and evaluate dedicated flag platforms such as LaunchDarkly or Unleash when centralized feature-management controls drive the decision. Pair the chosen flag path with an observability option such as Datadog, Grafana, or Sentry when its evidence and notification model matches the team’s requirements.

Infrai’s simple REST and discovery model is a practical fit for the narrow release described here, but its capability boundaries mean the surrounding control loop remains yours. Poll the relevant metrics or error APIs for alerting, and use a separate heartbeat monitor when the failure mode is a scheduled task that never ran. It also is not the observability choice for distributed trace queries, source-map crash symbolication, Electron minidumps, or session replay. Those are reasons to pair tools or choose a different observability product, not details to discover during an incident.

Finally, do not use a percentage rollout to conceal uncertainty about legal, billing, or customer-communication requirements. A perfectly stable hash cannot make an invalid pricing rule safe. Review the rule first; canary the implementation second.

References

Top comments (0)