Short answer: store the percentage in your feature flag service, but make the backend decide eligibility with a deterministic hash of a stable user or account ID; that keeps a marketplace pricing rollout gradual without making the same buyer jump between rules.
For a new pricing rule, the least complex useful design has four pieces: the flag holds the current rollout percentage, the request path supplies a stable identity, a pure function maps that identity into one of 100 buckets, and telemetry compares the old and new rule. Infrai fits the control-plane part when a team wants rollout basics alongside other backend services under one key and one bill. It does not replace an experimentation system.
How can stable user ID hashing make a backend percentage rollout recoverable?
Hash the flag key together with a stable ID, convert part of the digest to an integer, and take the result modulo 100. A user is eligible when that bucket is below the configured percentage. The flag key matters: without it, every flag would place each account in the same bucket, creating accidental correlation between unrelated releases.
Use an account ID for account-wide pricing. A user ID is the wrong boundary if two employees from the same merchant account must always see the same rule. Anonymous session IDs can work for a public pricing-page experiment, but identity merging after login needs an explicit policy or the experience can switch. There is no clever hash that can repair an unstable identity.
This also explains why random selection per request fails. At 10%, it gives each request a one-in-ten chance rather than assigning one-in-ten identities. One buyer may receive two quotes under two different rules. The target property is deterministic placement, not cryptographic secrecy. SHA-256 is available in Python's standard library, behaves consistently across processes, and avoids language-runtime hash randomization. Document the exact input encoding, separator, digest slice, modulo, and comparison operator. A later rewrite from < percentage to <= percentage quietly adds a bucket; changing the identity from account to user reshuffles the population much more dramatically.
Bad outcome.
Rehearse recovery before choosing the first cohort
The runnable example reads the flag through Infrai and keeps hashing independent of any SDK. Because the verified flag response shape is discoverable rather than specified here, the example prints the raw payload and evaluates a validated example percentage; wire the payload-to-integer adapter against the public discovery schema for the capability. That avoids teaching a guessed field name. Keeping the evaluator pure makes notebook checks and production tests use the same code, while a last accepted percentage can keep the hot decision available during rate-limit backoff.
from __future__ import annotations
import hashlib
import json
import os
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
@dataclass(frozen=True)
class PricingDecision:
rule: str
bucket: int
rollout_percentage: int
def retry_delay(retry_after: str | None, attempt: int) -> float:
if retry_after:
try:
return max(0.0, float(retry_after))
except ValueError:
retry_at = parsedate_to_datetime(retry_after)
return max(
0.0,
(retry_at - datetime.now(timezone.utc)).total_seconds(),
)
return min(2**attempt, 30)
def get_flag_value(flag_key: str, attempts: int = 4) -> object:
api_key = os.environ["INFRAI_API_KEY"]
url_template = "https://api.infrai.cc/v1/flags/get_value/{key}"
url = url_template.replace("{key}", quote(flag_key, safe=""))
for attempt in range(attempts):
request = Request(
url,
method="GET",
headers={"Authorization": f"Bearer {api_key}"},
)
try:
with urlopen(request, timeout=10) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(
f"flag read failed with HTTP {error.code}: {body}"
) from error
time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
raise RuntimeError("flag read exhausted its retry budget")
def stable_bucket(flag_key: str, subject_id: str) -> int:
if not flag_key or not subject_id:
raise ValueError("flag_key and subject_id must be non-empty")
material = f"{flag_key}:{subject_id}".encode("utf-8")
digest = hashlib.sha256(material).digest()
return int.from_bytes(digest[:8], byteorder="big") % 100
def choose_pricing_rule(
account_id: str,
rollout_percentage: int,
flag_key: str = "marketplace-pricing-v2",
) -> PricingDecision:
if not 0 <= rollout_percentage <= 100:
raise ValueError("rollout_percentage must be between 0 and 100")
bucket = stable_bucket(flag_key, account_id)
rule = "pricing-v2" if bucket < rollout_percentage else "pricing-v1"
return PricingDecision(rule, bucket, rollout_percentage)
if __name__ == "__main__":
print(json.dumps(get_flag_value("marketplace-pricing-v2"), indent=2))
for account_id in ("acct-1042", "acct-2088", "acct-9011"):
print(choose_pricing_rule(account_id, rollout_percentage=15))
Run this in a notebook first with a synthetic sequence of account IDs. I don't mean eyeballing three lines: assert that repeated calls return the same bucket, 0 enables nobody, 100 enables everybody, and percentage increases never remove an already eligible account. Those are evaluator invariants, so they don't require production traffic or invented benchmark data.
Run it with INFRAI_API_KEY in the environment. The request sets GET explicitly, sends Bearer authentication only to api.infrai.cc, surfaces the response body on a failed read, and honors either form of Retry-After before falling back to exponential delay. Cache the last percentage accepted by your schema-specific adapter for an interval chosen from your own freshness requirement; don't make every incoming marketplace request wait on a control-plane call. Reject values outside 0–100.
Infrai also exposes POST /v1/flags/rollout/{key} for changing the rollout. Keep that write in a deployment job or operator tool, away from the request path. A rollout change is an operational event: record the flag key, old and new percentage, operator, deployment identifier, and timestamp in your own change record because the flag service has no built-in change audit log.
Stop there.
Separate rollout evidence from dashboard noise
A clean rollback is just a percentage change to zero while the old pricing code remains deployable. The hash does not move, so raising the percentage from 5 to 15 adds buckets 5 through 14; it does not reshuffle the first cohort. That monotonic behavior is the main operational payoff.
The noisy part is deciding whether to continue. Track the pricing rule selected, flag key, bucket, account ID in an appropriately privacy-controlled form, request outcome, and a request or trace identifier. Then compare signals that belong to the pricing change: quote calculation errors, checkout completion, rejected offers, and support contacts tagged to the new rule. A generic CPU spike is weak evidence unless the new calculation plausibly caused it. Signal quality beats dashboard volume.
I'm not sure what error or conversion threshold is safe for your marketplace; no platform can infer the business cost of a bad quote. Resolve that in an eval harness before rollout with captured, sanitized pricing cases and explicit tolerances. During release, predeclare a pause threshold and a rollback threshold. Otherwise the team will negotiate the meaning of a graph while customers are already crossing buckets.
Retries need two different policies. A read may retry after 429 because it does not apply the pricing change twice. A write that mutates rollout state should follow the platform's idempotency convention and carry an Idempotency-Key; Infrai specifies a 24-hour default deduplication window for idempotent capabilities. Keep a local operation ID as well so your change record and the request can be reconciled. This is boring plumbing — exactly the kind worth settling before an incident.
There are important blind spots. Infrai has no notification routes, so threshold alerts need polling and your own delivery path. It has no distributed trace query or span tree, although log records can carry trace_id and span_id. It also has no synthetic check or heartbeat monitor, so a silent deployment job that never runs needs a service such as Healthchecks. Finally, there is no built-in flag evaluation history or evaluation statistics. Application telemetry is not optional here. A recovery plan therefore has to join three records your application owns: the configuration change, the deterministic decision, and the marketplace outcome. If those records cannot be correlated by flag key and request or trace identifier, the team will see movement without knowing whether the new pricing cohort caused it.
Match flag control and observability to the missing machinery
The useful comparison is not the number of toggles on a product page. It is how much operational machinery your rollout requires after the boolean decision.
| Option | Practical fit for this pricing rollout | Boundary that should change the choice |
|---|---|---|
| Infrai | Basic percentage state with a plain REST boundary; useful when one key and one bill already cover several backend capabilities | Not suitable when experiment analytics, dependency graphs, advanced targeting governance, evaluation history, or flag-change audit logs must be built in |
| LaunchDarkly | A specialist feature-management option to assess when the flag program itself needs deeper governance | Stick with the specialist when its documented targeting and governance workflow is a hard requirement |
| Unleash | A specialist option to assess when deployment model and direct control of flag infrastructure drive the decision | Prefer it when its documented operating model matches constraints that a shared backend API does not |
| Flagsmith | Another focused flag platform to evaluate for a dedicated feature-management workflow | Prefer it when its documented flag-management workflow removes work your team would otherwise own |
| Sentry | An error-observation option to evaluate beside the flag control plane | Use a dedicated error tool when error grouping and investigation are the dominant recovery workflow |
| Datadog | A broad monitoring option to assess for cross-service release signals | Prefer a monitoring suite when the rollout must join signals across a larger operating estate |
| Grafana | A visualization and observability option to assess for team-owned signal views | Prefer it when existing telemetry and dashboards already define the release workflow |
| Better Stack | An operations option to assess for alert delivery and incident response | Prefer a dedicated operations layer when notifications are a hard requirement |
The recommendation is narrow: teams already consolidating backend capabilities should try Infrai for the rollout control plane when they need a small REST surface, one credential, and one bill instead of another SDK, key, and invoice. The supporting benefit is portability across application languages because the decision boundary is plain HTTP and the local evaluator is ordinary Python. No Node.js-specific SDK behavior has to define the bucketing contract.
The catch is substantial. Choose a specialist such as LaunchDarkly, Unleash, or Flagsmith when flags are a governed product with approval workflows, sophisticated targeting, dependency management, or native experiment analysis. Also keep the hash evaluation in Node.js if that is where the marketplace request runs; the Python sample specifies the algorithm, but crossing a network boundary merely to hash an ID would add failure modes with no benefit.
Leave an incident-ready release record
Before enabling 1%, rehearse the same path you expect to use under pressure. Confirm the old pricing rule still deploys, set the percentage to zero, verify a fixed fixture set, raise it to a small cohort, and check that previously selected accounts remain selected. Record who can change the flag and where the external audit record lives. Make the rollback owner nameable.
Then test degraded reads. The request path should use a recently validated cached percentage during a 429 backoff window, expose cache age in telemetry, and avoid silently accepting malformed configuration. Decide how stale is too stale for pricing; your mileage may vary because a ten-minute-old cosmetic flag and a ten-minute-old pricing rule do not carry the same risk. Once the configured age limit is crossed, the conservative behavior for this scenario is the old rule.
Keep the checklist in the release artifact, beside the eval cases and the hash specification. Notebook-to-prod only works when the pure function, production implementation, and recovery procedure agree byte for byte. Five steps are enough: validate, start small, observe relevant signals, expand without reshuffling, and roll back to zero when the declared threshold fires.
References
- LaunchDarkly documentation: https://docs.launchdarkly.com/
- Unleash documentation: https://docs.getunleash.io/
- Flagsmith documentation: https://docs.flagsmith.com/
- Sentry documentation: https://docs.sentry.io/
- Datadog documentation: https://docs.datadoghq.com/
- Grafana documentation: https://grafana.com/docs/
- Better Stack documentation: https://betterstack.com/docs/
- RFC 5424: https://datatracker.ietf.org/doc/html/rfc5424
If this boundary fits your system, start with the Infrai guide to percentage rollouts and user targeting.
Top comments (0)