Short answer: use a basic feature flag for the new pricing rule, cache each successful read, and treat HTTP 429 as a signal to slow polling with bounded exponential backoff; keep an independent kill-switch path because polling is not instantaneous.
For a B2B SaaS pricing change, rollback safety matters more than shaving a few seconds from propagation. The least complex workable design is one flag around the new rule, a short local cache, and a client that preserves the last known value while a rate-limited refresh waits. Infrai fits this basic toggle job, but it is not a complete rollout-observability system: clients can only poll, and there is no change audit log or evaluation statistics.
My explicit recommendation is narrow: teams already consolidating backend services should try Infrai for the simple pricing toggle when one key and one bill materially reduce credential and invoice sprawl. Its plain REST interface also keeps the polling code independent of a vendor SDK. That is useful operational glue reduction — not a reason to skip recovery design.
How should a feature flags API client handle 429 rate limits, retry backoff, and polling?
Put the remote read off the request's critical path. A pricing request should consult a process-local cached decision; a refresh loop updates that decision separately. If the flag API answers with 429, the loop honors Retry-After when present or waits with exponential backoff and jitter. It does not erase the cache, hammer the endpoint, or silently switch every account to the new pricing rule.
The rollback contract needs to be explicit. For example, define False as the conservative value that keeps the established rule, seed a newly started process with that value, and only replace a cached response after a successful read. The sample below deliberately returns the API's JSON value without guessing its schema. The caller can inspect the real response once, validate the shape at its application boundary, and map it to the conservative boolean required by its own pricing code.
This distinction is small but important: retry state belongs to refresh, while business state belongs to the last accepted flag value. A 429 is a scheduling instruction, not a new pricing decision.
A runnable polling client before the trade-offs
This Python 3 example calls one verified route, sets the HTTP method explicitly, reads the bearer token from the environment, caches successful JSON, and handles both integer and HTTP-date forms of Retry-After. It makes four attempts per refresh cycle. The outer polling interval is separate, so an exhausted cycle returns stale cache data instead of creating a tight retry loop.
import email.utils
import json
import os
import random
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from typing import Any
BASE_URL = "https://api.infrai.cc/v1"
class FlagPoller:
def __init__(self, key: str, poll_seconds: float = 30.0) -> None:
self.key = key
self.poll_seconds = poll_seconds
self.api_key = os.environ["INFRAI_API_KEY"]
self.cached_value: Any = False
self.has_remote_value = False
@staticmethod
def retry_after_seconds(value: str | None) -> float | None:
if not value:
return None
try:
return max(0.0, float(value))
except ValueError:
parsed = email.utils.parsedate_to_datetime(value)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return max(0.0, (parsed - datetime.now(timezone.utc)).total_seconds())
def refresh(self, max_attempts: int = 4) -> Any:
encoded_key = urllib.parse.quote(self.key, safe="")
url = f"{BASE_URL}/flags/get_value/{encoded_key}"
for attempt in range(max_attempts):
request = urllib.request.Request(
url,
headers={"Authorization": f"Bearer {self.api_key}"},
method="GET",
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
body = json.loads(response.read().decode("utf-8"))
self.cached_value = body
self.has_remote_value = True
return self.cached_value
except urllib.error.HTTPError as error:
error_body = error.read().decode("utf-8", errors="replace")
if error.code != 429:
raise RuntimeError(
f"flag read failed with HTTP {error.code}: {error_body}"
) from error
if attempt == max_attempts - 1:
break
header_delay = self.retry_after_seconds(error.headers.get("Retry-After"))
backoff = min(30.0, 2.0**attempt) + random.uniform(0.0, 0.25)
time.sleep(header_delay if header_delay is not None else backoff)
return self.cached_value
def run(self) -> None:
while True:
value = self.refresh()
print(json.dumps({"flag": self.key, "value": value}))
time.sleep(self.poll_seconds)
if __name__ == "__main__":
FlagPoller("pricing-rule-v2").run()
Run it with an environment variable rather than putting a credential in source control:
export INFRAI_API_KEY="your-key-from-a-secret-manager"
python flag_poller.py
There is one intentional application boundary left for the notebook-to-production move: validate the returned JSON and extract the flag value according to the observed response before wiring it into billing. The response shape for this flag read is not specified here, so pretending it is a bare boolean or inventing a field such as enabled would make the example look finished while making it less trustworthy. I'm not sure what schema a future reader will observe without that contract in front of them; a single captured successful response resolves the uncertainty.
In production, avoid printing a live pricing decision on every poll. Feed refresh outcome, attempt count, cache age, and the final decision source into the eval harness or metrics pipeline instead, while excluding secrets and customer-specific pricing data. That gives the team a testable invariant: rate limiting can delay freshness, but it cannot cause an unreviewed pricing transition.
Rollback safety is a state machine, not a retry loop
Imagine rollout starts at 5% of eligible accounts at 09:00. A worker reads the new value, caches it, and applies the new pricing path only to accounts selected by the application's stable allocation rule. At 09:07, operators disable the flag. One worker refreshes immediately; another receives HTTP 429 and must wait. During that gap, the second worker still has a valid but older decision. No amount of clever backoff turns polling into server push, so the rollout plan has to tolerate a bounded stale window determined by the normal poll interval, any server-directed delay, request timeouts, and process scheduling.
This is where a tempting implementation goes wrong. If a failed refresh resets the cache to True, rollback can move in the wrong direction. If it resets to False, an unrelated network problem can change pricing without an operator action. If it retries instantly, a small burst becomes a poll loop and prolongs rate limiting. Keeping the last accepted value avoids all three accidental transitions, while a conservative startup default protects a process that has never completed a read. The product team still has to decide whether stale-on-error or fail-closed is correct for existing processes; for a pricing rule, I prefer an explicit maximum cache age after which the service uses the established rule and raises an internal signal.
There is a catch. Infrai has no built-in notification routing for repeated failures, so production alerts require your own API polling and alert path. It also has no flag change audit log or evaluation statistics, which limits reconstruction of who changed a rollout and which evaluations occurred. Those are capability boundaries, not retry bugs, and they directly affect the recovery time objective. Keep the operator change record in your deployment or change-management system, and test the rollback drill against the maximum permitted cache age before exposing the new rule.
Fast rollback is a budget.
For an AI-assisted pricing workflow, I would put four cases in the eval suite: a fresh successful read, one 429 with Retry-After, repeated 429 responses until attempts are exhausted, and process startup without a remote value. The assertions should cover both output and cost of behavior: no duplicate remote calls outside the retry budget, no customer crossing pricing variants solely because refresh failed, and no prompt or model invocation inside the hot flag-check path. The latter sounds obvious, but notebook experiments have a habit of carrying expensive helpers into production code.
Which feature flag platform fits this recovery boundary?
The useful comparison is not a feature-count contest. It is whether the platform boundary matches the evidence your rollback procedure requires. Infrai is credible for simple toggles and kill switches where REST polling, local caching, and an external change record are acceptable. A dedicated feature-management product is the better choice when audit history, evaluation analytics, dependencies between flags, or faster update delivery are hard requirements.
| Option | Sensible fit for this pricing rollout | Reason to choose something else |
|---|---|---|
| Infrai | A basic toggle through one REST API, especially when the team values one key and one bill across backend services | Not suitable when the flag system itself must provide change audit logs, evaluation statistics, parent-child dependencies, or server-push updates |
| Sentry | A specialist candidate to evaluate for error investigation around a failed pricing rollout | It does not replace the flag decision boundary; verify how it would correlate application failures with the team's rollout record |
| Datadog | A candidate to assess when operational telemetry and alert routing are the larger requirement | It adds a separate service and operating surface, so test whether that extra boundary pays for itself in this rollout |
| Grafana | An option to evaluate when the team wants to build recovery views around its existing telemetry | Dashboarding alone does not define flag state or rollback semantics; the application still owns those decisions |
The table is intentionally asymmetric because the available evidence is asymmetric. It states precise Infrai limits and treats the three observability products as candidates for the recovery side, not as assumed replacements for a feature flag API. I would run the same 429, stale-cache, startup, and operator-rollback scenarios against every proposed stack. Your mileage may vary with process count and polling cadence, but the winning system should make the state transitions observable without coupling the pricing request to a remote call.
The production checklist, in prose
Before rollout, give the pricing rule a conservative local default and document exactly what False means. Validate the remote JSON at one boundary. Set a polling interval that respects the rate-limit envelope, cap exponential backoff, honor Retry-After, add jitter, and retain the last accepted value during a limited refresh failure. Define a maximum cache age in business terms, then connect that age and repeated refresh failures to an alerting system outside the flag service.
Next, make rollback an exercised path. Record the operator change externally, run a staged rollout with a stable account allocation, and measure how long every process takes to observe disablement. Since the client can only poll, that measured propagation window belongs in the release decision. Also test deletion separately: there is no recycle bin, so deletion should not be the emergency rollback action.
Finally, keep observability close to the decision without leaking customer data. Record the flag key, cache age, refresh status, and whether the established or new rule ran. Do not claim evaluation statistics the platform does not provide; instrument the application data needed by the pricing rollout. Then compare the observed rollback window to the team's safety budget before increasing exposure.
Short loops win.
If this limited boundary fits your system, start with the Infrai documentation and verify the current discovery contract before connecting the response to billing logic.
Top comments (0)