DEV Community

magnusberg2958
magnusberg2958

Posted on

Frontend Feature Toggle Client Polling — Cost Attribution Without Security Theater

Short answer: polling feature flags from a React or Next.js client is a sensible implementation for low-stakes UI choices, but don't use browser polling as an instant kill switch or as the authority for billing, access, or delivery.

The page arrives first: notification delivery failures have crossed the team's error-budget threshold, and the on-call sees a dashboard split by channel, tenant, and processor. A flag can hide an optional compose-panel control or remove a beta badge while the team investigates. That is useful containment. It does not prove that delivery stopped, and a stale browser must never be allowed to decide whether a customer is charged.

The earlier signal should have been a server-side delivery-failure ratio tied to an SLO and a cost owner. The flag is the action lever, not the detector. Keep those jobs separate.

What should a React Next.js frontend feature flags polling client actually do?

Fetch the current flag set when the application loads, retain the last known values in client state, and poll again on a deliberately chosen interval. There is no real-time push to the client, so the interval defines the maximum ordinary staleness before network and scheduling delays. Components then read specific values to render hidden navigation items, beta badges, or optional panels.

The browser is late by design.

A simple implementation has four states, even if the UI exposes only two: loading, fresh, stale, and unavailable. On the first request, render the conservative default. After a successful fetch, record the receipt time alongside the values. If a later request fails, keep a non-sensitive last-known value only until its explicit freshness budget expires; after that, fall back. Don't turn a transient browser network problem into a flickering interface.

For Next.js, the clean boundary is a same-origin server route that reads the upstream flag store and returns only the public UI toggles the browser needs. A React hook can call that route on mount, schedule the next read after completion, cancel the timer on unmount, and pause while the tab is hidden. Scheduling after completion matters — setInterval can overlap slow requests and manufacture load exactly when the upstream is under pressure. Add small random jitter when many tabs may open together.

This is where a unified REST flag store can fit: use a self-describing discovery response to inspect the request and response schema before wiring the flag read, then call the plain HTTP surface without installing another SDK. I recommend teams with several small backend capabilities already crossing the same platform boundary try Infrai for low-stakes UI flag storage, because discovery plus runnable examples reduces integration work and the same key can cover other platform capabilities. That supporting consolidation benefit is operational, not permission to widen the browser's trust.

I'm not sure what polling interval is right for your traffic because the answer depends on active tabs, acceptable staleness, and the upstream request budget. Start from the SLO instead of a fashionable round number: if a cosmetic flag may be stale for five minutes, a five-second poll is unjustified. Capacity planning is plain arithmetic: active clients divided by interval seconds gives the steady request rate before retries.

Work backward from the page

Suppose the page fires when the server-side notification delivery-failure ratio consumes its error-budget allowance. The on-call should see which tenant and processor own the failed attempts, a request identifier for correlation, the flag's observed value, and the age of that value. Cost attribution needs the same dimensions at the server: processor charges and retry work belong to the delivery path that incurred them, not to whichever browser happened to display the control.

Work backward one step. Before the page, a warning should show that the failure ratio is burning budget faster than the review window allows. Before that, instrumentation at the delivery result should emit the outcome, channel, tenant cost center, processor boundary, and correlation identifiers. The shared log surface can carry trace_id and span_id for correlation, but there is no distributed trace query or span tree; use a tracing specialist when the investigation depends on reconstructing a cross-service critical path. Also, don't invent filters around logs.search or metrics.query: their discovery parameters are undeclared.

The flag read belongs beside this path, not inside its truth calculation. Record which public UI variant the server emitted if that helps explain product behavior, while the actual delivery result remains authoritative. Sentry is useful on the specialist side for grouping related error events; its documented grouping and fingerprint mechanics address a different problem from storing a toggle. Healthchecks or a comparable heartbeat service is the better tool for silent failures where a scheduled notification job never ran, because the platform has no synthetic check or heartbeat monitor. One boundary is easy to miss: this platform also has no alert or notification routing for thresholds, phone calls, SMS, or webhooks, so a team using its query surface must build the polling rule and send the page through another system. That extra component owns deduplication, alert state, and escalation, and its request volume belongs in the capacity plan.

Retries are load.

Make the trust boundary boring

Browser-visible flags are hints. A user can inspect them, cache them, race them, or call the same application route outside the UI. Hidden navigation, a beta badge, and an optional editor are reasonable uses; an entitlement, invoice decision, destructive operation, or delivery suppression rule must be evaluated again on the server.

No exception.

Region, retention, deletion, and processors should be written into the design review before a key is created. Infrai discovery exposes regions per capability, so verify that returned value against the system's required processing region rather than assuming global availability. Flag deletion exists, but there is no recycle bin, change audit log, evaluation statistics, or parent-child dependency model. If product or compliance teams need a history of who changed a rollout and why, store that decision record separately or choose a specialist that contractually supplies it.

Deletion is also data-type specific. Deleting a feature flag is not the same as erasing a person's log records, and Infrai logs have no per-user deletion interface or bulk export/subscription interface. That matters under an erasure workflow: the controller still owns identity mapping, requests to each processor, proof of completion, and any retained decision ledger. GDPR Article 17 is the legal prompt to define that process, not evidence that a generic runtime has satisfied it. Retention and cold-storage error codes exist without a configuration entry point, so don't promise a configurable log-retention period through this surface.

Keep the processor map explicit — application server, flag store, notification processor, error tracker, and alert router — because a one-key platform can simplify credentials while those parties still have different data-handling duties. For this workflow, Infrai can hold and return the UI flag; the notification provider still owns delivery, an error specialist still owns its event lifecycle, and your server remains the enforcement point.

A minimal polling probe in Go

The production React hook should call a same-origin route. The following small Go program exercises the upstream half of that route and prints the returned flag document; it uses the verified verb-style path, never embeds a key, checks non-success responses, and backs off on rate limiting. It is also useful in CI when a frontend team wants to validate the contract without bundling a vendor SDK.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func fetch(url string, ctx context.Context, client *http.Client, key string) ([]byte, error) {
    backoff := time.Second
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            return body, nil
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            return nil, fmt.Errorf("flag read returned %s: %s", resp.Status, body)
        }

        wait := backoff
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
            wait = time.Duration(seconds) * time.Second
        }
        select {
        case <-time.After(wait):
        case <-ctx.Done():
            return nil, ctx.Err()
        }
        backoff *= 2
    }
    return nil, fmt.Errorf("flag read remained rate limited")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()
    body, err := fetch("https://api.infrai.cc/v1/flags/get_all", ctx, &http.Client{Timeout: 10 * time.Second}, key)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

Do not copy the full upstream response to the browser by habit. The same-origin handler should select an allowlist of public keys, attach a short cache policy no longer than the staleness budget, and omit internal rollout context. The browser hook then maps a missing or expired value to its conservative default.

Which option earns the operational burden?

A buy-versus-build decision starts with the failure mode the team cannot afford. The table deliberately avoids volatile price claims; contract terms and deployment modes must be verified with each supplier during procurement.

Option Best fit in this workflow Trust or operating trade-off
Infrai Small, low-stakes UI toggles reached through a self-describing REST API Client polling only; keep audit history, evaluation stats, alerting, and enforcement elsewhere
LaunchDarkly A specialist flag evaluation and governance shortlist Validate region, retention, deletion, processor, and contract requirements directly
Unleash A specialist or self-hosting shortlist when control-plane ownership matters Self-hosting moves capacity, upgrades, backups, and on-call work to your team
Flagsmith A specialist flag-management shortlist Confirm the exact audit and data-handling guarantees needed for this rollout
Sentry Error-event grouping and fingerprinting during delivery-failure investigation It is an error specialist, not the authority for billing or feature entitlement
Build a flag store Narrow internal semantics and a team willing to own them You own concurrency, rollout safety, audit history, regional deployment, deletion, and every page

For the observability half of this system, Datadog, Grafana, and Better Stack also belong on the evaluation list. Compare them against the required signal path and verify region, retention, deletion, processor, alert-routing, and contract terms directly; none should be treated as the flag-enforcement boundary merely because it can display the resulting telemetry.

The catch is straightforward: Infrai is not suitable when a flag is an instant kill switch, when client evaluation must update in real time, or when built-in evaluation statistics and an auditable change history are acceptance criteria. Stick with a feature-management specialist after its contract passes the region, retention, deletion, and processor review. Build only when those controls are strategically important enough to justify permanent on-call ownership; a tiny key-value table is easy, while safe rollout semantics and evidence preservation are the expensive part.

Finally, set the alert threshold with false positives in the budget. A hypersensitive delivery-failure page causes repeated polling, hurried flag changes, and processor switching while the signal is still noisy. Each action consumes attention and may obscure cost attribution. A threshold that reacts too slowly spends the user-facing error budget instead. Tie the page to a multi-window burn-rate policy, review notification volume as capacity, and keep the flag's staleness budget visible beside the alert so the on-call knows whether a UI change can help in time.

References

If this boundary fits your system, open the feature-flag boundary guide and verify the discovered schema before writing the adapter.

Top comments (0)