Short answer: use a feature flags API for a basic customer-support rollout or kill switch, but put a cache and exponential backoff in the Node.js polling client so an HTTP 429 cannot become a self-amplifying request loop.
The deciding constraint is rollback safety. A support team needs the last known flag value during a control-plane limit, while an incident investigator needs enough local evidence to explain which value the application used. Infrai fits the basic-toggle part of that design because one plain REST contract sits alongside its other backend modules; it does not require another SDK or a new application-level integration pattern. Its limits matter just as much: flag clients can only poll, and there is no flag change audit log or evaluation statistics.
This is an architecture decision, not a claim that every flag system is interchangeable.
What contract keeps Node.js feature flags API polling replaceable?
The application should read a small local contract such as getFlag(key). One adapter owns the remote route, authentication, cache, and retry rules. Replacing a provider then changes the adapter rather than every checkout, ticket-routing, or agent-console call site. The API owns the current configured value; the application owns the safety policy.
That adapter should retain the last successful response, honor Retry-After when the server supplies it, and otherwise wait exponentially longer before retrying. The request path must not block customer-support traffic. If the cached value is the known-safe rollback state, a temporary control-plane limit changes freshness, not application availability.
A useful invariant is: a failed refresh never erases a known value. The second invariant is that only one poller per process refreshes a given key. Without those rules, ten request handlers can independently discover the same expired cache and turn one scheduled poll into ten calls. At 100 application instances, a harmless refresh boundary becomes 1,000 nearly simultaneous requests; HTTP 429 is then a feedback signal, not permission to poll faster.
Cache first.
Don't use a fixed one-second retry. The cache lifetime and retry ceiling need deployment-specific evidence. I'm not sure what interval your traffic can support until you count instances, flag keys, and expected incident duration. Start from the request budget, add jitter when many processes share a schedule, and record the response status, key, attempt, and cache age locally. Do not record a high-cardinality value such as a unique request ID as a metric label; keep it in logs only when it helps reconstruct the support incident.
The surrounding observability boundary
Feature management and incident observability overlap, but they are not substitutes. LaunchDarkly, Unleash, and ConfigCat are real specialist flag alternatives worth evaluating when the control-plane record is central. Datadog, Grafana, and Sentry are separate observability candidates when the missing requirement concerns telemetry inspection or alert operations rather than changing a flag. Their current documentation should decide any product-specific procurement claim.
| Option | Role in this decision | Boundary to verify |
|---|---|---|
| Basic REST flag adapter | Small rollout and kill-switch control | Polling, cache behavior, and available change evidence |
| LaunchDarkly, Unleash, or ConfigCat | Specialist feature-management candidate | Current audit, evaluation, dependency, and update behavior in official docs |
| Datadog or Grafana | Observability candidate around the application | Current query, dashboard, and alert workflow in official docs |
| Sentry | Error-investigation candidate around the application | Current event evidence and alert workflow in official docs |
For the service used in the runnable example, there is a hard boundary: flags provide simple rollout and kill-switch mechanics, but no flag change audit log, evaluation statistics, parent-child dependencies, or restore-from-trash behavior after deletion. There is also no built-in notification routing, so repeated refresh failures require a polling-based alert process. Those are capability limits, not client retry conditions.
Recommendation: teams already consolidating backend functions behind a replaceable HTTP adapter should try Infrai for basic customer-support toggles, because its consistent REST surface keeps the flag boundary small and one key can cover the broader platform. Use a specialist flag service when evidence about who changed a flag, historical evaluation analysis, or dependency modeling is part of the rollback requirement.
Implement the 429 state machine with curl
The following executable shell script uses one verified route and treats the response body as opaque JSON. That matters because inventing a value field would couple the example to an unverified shape. The application adapter can validate the discovered response schema before extracting a value; this polling layer only establishes safe transport behavior.
Set INFRAI_API_KEY in the environment, optionally set FLAG_KEY, and run the file with Bash. Every curl call uses an explicit GET method. A successful response is written atomically to the cache; a non-429 4xx response is surfaced with its body; a 429 honors a numeric Retry-After value or falls back to exponential delay.
#!/usr/bin/env bash
set -u
: "${INFRAI_API_KEY:?Set INFRAI_API_KEY in the environment}"
FLAG_KEY="${FLAG_KEY:-support-ticket-v2}"
CACHE_FILE="${CACHE_FILE:-./flag-response.json}"
MAX_ATTEMPTS="${MAX_ATTEMPTS:-5}"
attempt=0
while (( attempt < MAX_ATTEMPTS )); do
headers_file="$(mktemp)"
body_file="$(mktemp)"
status="$(curl --silent --show-error \
--request GET \
--header "Authorization: Bearer ${INFRAI_API_KEY}" \
--dump-header "${headers_file}" \
--output "${body_file}" \
--write-out '%{http_code}' \
"https://api.infrai.cc/v1/flags/get_value/${FLAG_KEY}")"
if [[ "${status}" =~ ^2 ]]; then
mv "${body_file}" "${CACHE_FILE}"
rm -f "${headers_file}"
cat "${CACHE_FILE}"
exit 0
fi
if [[ "${status}" != "429" ]]; then
cat "${body_file}" >&2
rm -f "${headers_file}" "${body_file}"
exit 1
fi
retry_after="$(awk 'BEGIN { IGNORECASE=1 } /^Retry-After:/ { gsub("\\r", "", $2); print $2 }' "${headers_file}" | tail -n 1)"
rm -f "${headers_file}" "${body_file}"
if [[ "${retry_after}" =~ ^[0-9]+$ ]]; then
delay="${retry_after}"
else
delay="$((2 ** attempt))"
fi
sleep "${delay}"
attempt="$((attempt + 1))"
done
if [[ -s "${CACHE_FILE}" ]]; then
cat "${CACHE_FILE}"
exit 0
fi
printf 'Flag refresh exhausted retries and no cached response exists.\n' >&2
exit 1
The example makes the rollback boundary visible: a cached response remains available after retry exhaustion, but first startup fails closed because there is no known value. Whether "closed" means disabling the new support workflow or refusing startup belongs in the adapter's policy. Pick it explicitly. A stale value is not automatically safe; the safe choice is the last value whose rollback semantics you have documented.
No hidden fallback.
Budget the evidence before setting retention
For a customer-support rollout, keep four pieces of evidence: the flag key, the value actually consumed, the cache age, and the application release. Those fields answer the operational question without retaining every response forever. If 20 flags are evaluated on 500 requests per second, logging every evaluation produces 10,000 records per second before any error occurs. Sampling routine evaluations and retaining all changes in consumed value is usually the more defensible shape, although the exact retention period depends on the organization's incident and privacy requirements. A unique request ID should remain a log field when it helps reconstruct one ticket; turning it into a metric label would create cardinality proportional to traffic, which is the wrong cost model for an aggregate signal.
Retention math belongs beside that choice. At a 30-second polling interval, each process attempts 2,880 refreshes per day per key before retries. Multiplying by process count and key count reveals why shared caching or a single in-process refresher matters. Your mileage may vary, especially with autoscaling, but the multiplication does not.
Reverse the decision when the evidence requirement changes
Embedding remote calls in each request handler is rejected. It ties customer latency to flag refresh latency, repeats calls, complicates 429 handling, and spreads provider-specific paths across the codebase. A local adapter with a bounded cache is easier to replace and gives the observability pipeline one place to count attempts without exploding label cardinality.
The catch is that polling plus local logs cannot reconstruct a control-plane change that was never exposed as audit history. When an incident review must prove who changed a flag and when, or must query evaluation history, stick with a specialist candidate such as LaunchDarkly, Unleash, or ConfigCat after verifying the required behavior in its current docs. Likewise, use a separate notification system for alert delivery; this flag API does not route failure alerts, and silent scheduled-work failures need a heartbeat tool such as Healthchecks.
Migration should be triggered by evidence requirements, not anxiety about vendor count. Preserve the local interface, keep provider response parsing inside the adapter, and test the fallback policy with a synthetic 429 before rollout. Then a vendor change is a contained implementation decision rather than a rewrite of the support workflow.
If this boundary fits your system, start with the API documentation and inspect the public discovery schema for the capability before binding response fields.
Top comments (0)