Short answer: poll remote feature flags to control the React experience, but alert on the scheduled import's last successful result, not on flag fetches; keep a conservative local fallback, add randomized refresh timing, and instrument three bounded signals: configuration age, import-result age, and flag evaluation outcome.
That separation resolves the central trade-off. A fast polling interval can make a switch feel responsive while multiplying requests and telemetry, yet it still cannot prove that a logistics import produced anything. A quiet flag client can be perfectly healthy while yesterday's shipment records remain the newest data in the system. Signal quality comes from measuring the business event whose absence matters.
In this design, the flag controls whether the frontend exposes a newly imported view. The import-result signal decides whether operators should be paged. They meet in the user experience, but they have different clocks, failure meanings, and retention needs.
Keep those clocks separate.
How should a React frontend poll a feature flags API with fallback config?
Treat the browser's local fallback as a small, reviewed policy, not a stale copy of every server-side flag. For a logistics screen that depends on scheduled imports, a safe fallback might keep the new view hidden until a valid response explicitly enables it. That choice is intentionally conservative: it protects the workflow from an unknown configuration state, though it can delay exposure during a control-plane interruption. A flag whose safe behavior is "on" should encode that decision explicitly rather than inherit a universal default.
The polling loop needs five states even if the UI renders only two: initial, fresh, stale, rejected, and offline. "Rejected" means the response failed schema or value validation; "offline" means no usable response arrived within the client's deadline. Neither state should be confused with the flag's false value. Collapsing transport status and evaluated value into one Boolean makes debugging cheap to implement and expensive to operate.
Use a monotonic sequence or version supplied by the configuration service, retain the last validated document in memory, and replace it atomically. The frontend should apply a response only after validating the complete document. If the response is absent or invalid, it continues with the defined fallback or last validated value according to a documented maximum age. Your mileage may vary on that maximum age because a display-only flag and a workflow-changing flag carry different operational risk.
Don't synchronize every open tab to the same boundary. Add bounded random variation to the interval, pause or reduce polling for background tabs when product requirements permit, and trigger a refresh when a tab becomes active. Those are client policies, not evidence that the scheduled import succeeded.
The API contract can be probed independently of the component before deployment. This example uses a pseudonymous endpoint and sends the current version so the service can decide whether a new document is needed:
curl --fail-with-body \
--header 'Accept: application/json' \
--header 'If-None-Match: "flags-v42"' \
--max-time 3 \
https://config.example.test/feature-flags
The important contract is behavioral: a validated document replaces the current one; no document leaves the current policy intact until its age limit; an expired policy selects the reviewed fallback. The UI should expose none of these transport details to a dispatcher unless they affect an action the dispatcher can take.
The alert belongs to the import result
A scheduled-import monitor should model an expected result window. Suppose an illustrative route manifest import is scheduled every 15 minutes and the team permits 10 minutes of processing delay. The useful state is not "the scheduler ran" or "the flag endpoint answered." It is whether a successful, nonempty result has arrived within the agreed window, with an exception for an intentionally empty upstream interval if the domain supports one.
That last clause matters. Zero records can mean a broken feed, a quiet depot, a filter mistake, or a legitimate empty batch. The monitor needs a domain outcome, such as success_with_rows, success_empty_expected, success_empty_unexpected, or failed, rather than an unlabeled success bit. These are proposed states for the application contract, not a claim about a particular service.
Page on result age after the grace window. Send a lower-urgency notification when configuration age exceeds its policy but the fallback preserves the established workflow. Record polling rejection for diagnosis, but don't page on a single rejected response. The alert hierarchy then follows user impact instead of whichever component emits the easiest counter.
This is the hard distinction: availability is not freshness.
The notification should carry the schedule identifier, expected window, last successful result time, last observed outcome, and a trace or run correlation identifier if the import pipeline already has one. Avoid embedding customer, depot, shipment, or file identifiers as metric labels. Prometheus's instrumentation guidance warns against high-cardinality labels and specifically cautions that every unique label set creates another time series. Put investigative identifiers in bounded-retention logs or traces instead, where access and deletion policy can be applied deliberately.
Three bounded signals beat a wall of events
Start with three metric families and justify any fourth. flag_config_age_seconds is a gauge by deployment or environment, not by user. import_result_age_seconds is a gauge by schedule class or another deliberately bounded grouping. flag_evaluations_total is a counter with a small outcome set such as enabled, disabled, fallback, or rejected. The names are examples; the cardinality budget is the design.
Count the series before shipping. If there are 4 environments, 3 schedule classes, and 4 evaluation outcomes, an illustrative upper bound is small and reviewable. Adding customer_id with 20,000 possible values changes the order of magnitude immediately. Adding a raw error message is worse because the set can grow without an obvious ceiling. Exact storage cost still depends on scrape cadence, compression, retention, and the chosen backend, so I'm not sure a byte estimate is defensible until those inputs are measured in the actual system.
Logs should answer why, not duplicate every poll. Emit a structured event when the client changes configuration version, rejects a document, enters fallback, or recovers. Repeated identical failures can be aggregated at the client or collection tier, provided the aggregate preserves a count and first/last timestamps. For server applications using Logback, its appender model is one documented extension point for directing events, but custom transport logic also creates buffering, retry, and shutdown responsibilities. The same trade-off exists in other logging stacks even though the implementation hook differs.
Retention follows the question each signal answers. A short window of detailed rejection events may be enough for a deployment investigation, while a lower-cardinality result-age series may deserve longer retention for schedule reliability analysis. Keeping both for the longest available period spends storage without improving the decision. Sample routine success detail first; preserve every state transition and alert boundary. Never sample the only record that an import stopped producing results.
There is a catch: this minimal set is not suitable when flags are used for formal experiments, per-tenant entitlements, or safety-critical authorization. Those cases require different audit and analysis contracts, and the frontend flag must not become the authorization control. Likewise, stick with server-pushed updates when the allowed reaction time is shorter than a practical browser polling interval and the operating team is prepared to own connection lifecycle and fan-out behavior. Polling wins simplicity, not every latency contest.
Compare designs after defining the signal
Three architectures are reasonable once the result-age alert is fixed. Periodic browser polling has predictable client behavior and works through ordinary request infrastructure, but request volume grows with active clients and the control plane learns little about whether an import is healthy. A server-push channel can reduce configuration propagation delay, but it adds connection state, reconnection behavior, and capacity questions. Server-side evaluation keeps configuration away from the browser and can align flags with backend decisions, but each relevant page response must carry the evaluated state or obtain it through another application endpoint.
None of them replaces import observability.
Choose by reaction-time objective, number of active clients, tolerance for stale configuration, and operational appetite. Then calculate the telemetry consequence. Per-poll access logs can dwarf the few state changes anyone investigates. Per-user metrics turn a bounded control signal into a cardinality problem. Full tracing of every refresh may be defensible during a short rollout, but continuous head sampling should be set from an explicit diagnostic target rather than habit.
The cheapest event is the one that was never emitted, but cost alone is a poor filter. Keep the event if losing it would prevent a specific operational decision. Drop, aggregate, or sample it when another retained signal answers the same question. This test protects signal quality while putting a real boundary around ingestion and retention.
Roll out the policy without hiding failures
Begin by running the freshness calculation without paging. Compare its state transitions with known import outcomes, especially legitimate empty batches and delayed schedules. Then enable a low-urgency notification, tune the grace window from observed completion times, and only afterward connect the result-age breach to paging. The flag client can ship separately with fallback and configuration-age telemetry enabled from its first release.
Test four conditions before broad deployment: a valid configuration change, an invalid document, an unreachable configuration service, and an import that runs without producing an acceptable result. Verify that the first changes the UI atomically, the next two select the documented fallback behavior, and the last one creates the import alert regardless of flag status. Also verify series counts in a staging inventory; the expected label combinations should be enumerable on paper.
Keep the rollback small. The established logistics view remains available behind the conservative fallback, while alert routing can return to notification-only without changing the import job. After rollout, review stored bytes, series count, event volume, and useful investigations at the end of one retention cycle. A signal that never informs an action should have to defend its place in the next cycle.
The final rule is compact: poll for configuration, measure results for health, and retain only enough evidence to distinguish the two.
Top comments (0)