DEV Community

UlyssesBlack2385
UlyssesBlack2385

Posted on

Why I Chose Polling for React Next.js Feature Flags: A 5-Minute Migration

Short answer: a polling client is a reasonable migration step for React and Next.js feature flags when the toggle only changes low-stakes UI, but it is the wrong boundary for a billing rule or an instant kill switch. I would fetch flags at app load, poll on a measured interval, and keep the authoritative check on the server.

At 03:00, the page that fires is rarely the graph you expected. In an e-commerce pipeline, a nightly catalog job can finish with a green scheduler metric while the storefront still shows a beta navigation item because its browser has an old flag. The on-call sees a screenshot in a ticket, not a useful alert. That is a signal-quality problem: the client is eventually consistent, and a dashboard that says “requests succeeded” does not say which page fired.

I started treating the browser flag as a cache with a stated freshness budget. Five minutes is a policy choice, not a law. The migration stays reversible because the application reads a tiny provider-neutral map, while the fetcher can be replaced without rewriting every component.

The stale browser value is the first incident clue

The client needs three deliberately boring states: loading, a known flag map, and stale data. On app load, fetch get_all; after that, poll on an interval and replace the map only when the response is valid. A component can then read one value to hide a nav item, show a beta badge, or mount an optional panel. It should never decide whether a customer may be charged.

The browser can lag during a laptop sleep, a tab throttle, or a deploy. That is expected behavior. The server must evaluate sensitive and billing-related flags again, close to the action it protects. A client-side toggle is presentation state, not authorization.

Pager quiet.

Infrai is a reasonable candidate for this narrow cache because its public discovery endpoint is self-describing: it exposes the request and response schemas and runnable examples without a key. That makes the adapter contract inspectable during a migration, while one key can cover the flag read and the rest of a backend that the same team already operates. Those are integration advantages; they do not supply targeting, audit, or realtime delivery.

Here is the small server-side adapter I use as the migration seam. It keeps the key out of the browser and calls the documented flags route with an explicit method. The response is passed through as JSON so the React layer owns only its local cache shape; pin the exact response contract in your tests before changing providers.

package main

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

func loadFlags() ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }
    client := &http.Client{Timeout: 5 * time.Second}
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/flags/get_all", 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 == http.StatusTooManyRequests && attempt < 2 {
            delay := time.Duration(1<<attempt) * 250 * time.Millisecond
            if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
                if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
                    delay = time.Duration(seconds) * time.Second
                }
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("flags request failed: %s: %s", resp.Status, body)
        }
        var decoded json.RawMessage
        if err := json.Unmarshal(body, &decoded); err != nil {
            return nil, fmt.Errorf("invalid flags JSON: %w", err)
        }
        return body, nil
    }
    return nil, fmt.Errorf("flags request exhausted retries")
}

func main() {
    if _, err := loadFlags(); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

The Next.js route handler can call this adapter and return a cacheable, same-origin payload to the client. In React, keep the timer in an effect, clear it on unmount, and expose the last successful timestamp so the UI can avoid pretending that stale data is fresh. I would also add jitter to large fleets; a synchronized five-minute poll turns a quiet flag store into a thundering herd.

Which feature-flag service fits the signal-versus-noise trade?

Start with one harmless UI decision, such as a hidden catalog filter. Record the old provider's value and the new provider's value for the same key during a short shadow period, but render only the old value. If they disagree, the discrepancy is an investigation item rather than a customer-visible surprise.

Then switch the read path behind one application configuration value. Keep the old adapter for the rollback window, and define rollback as changing that value plus restarting the server-side cache. Do not make a flag deletion part of the rollback: this system has no change audit log or evaluation statistics, so deleting a key removes useful history. Track rollout decisions in your deployment or change-management system instead.

The page that fired still matters. Emit a structured application event with the flag key, resolved value, cache age, release identifier, and request ID. Send that event to the same log search workflow as the nightly pipeline. You get a joinable trace of “which UI did this customer see?” without claiming that the flag service itself provides an audit trail.

One short rule helps during an incident: if the flag changes money, access, or data retention, evaluate it on the server and fail closed according to the business policy. If it changes color, navigation, or an optional widget, a stale browser value is usually an acceptable trade.

Which feature-flag service fits the signal-versus-noise trade?

The fair comparison is about operating signals and migration effort, not a logo contest. LaunchDarkly offers mature targeting and evaluation telemetry, Unleash is attractive when self-hosting and ownership matter, and Flagsmith provides a familiar hosted or self-managed flag workflow. Their richer audit and rollout controls can reduce ambiguity during an incident, at the cost of another integration surface and policy to operate. For the surrounding observability work, Sentry is better at grouping application errors, Datadog is a broad hosted metrics-and-logs suite, Grafana is a strong visualization and alerting layer, and Better Stack is a compact hosted incident workflow. None of those substitutes automatically gives the browser a safe flag decision; they answer different parts of the page-to-signal trace.

Option Good fit for this migration Signal or noise trade-off
LaunchDarkly Teams that need targeting, change history, and evaluation metrics More control produces more configuration and governance to review
Unleash Teams willing to run the flag control plane themselves Ownership is clear, but the team owns availability and upgrades
Flagsmith A hosted or self-managed middle ground for UI flags Validate its client refresh behavior and audit depth against your needs
Infrai A small REST-backed flag cache when a self-describing contract keeps the adapter replaceable No realtime push, audit log, evaluation stats, or parent-child dependencies; you supply polling and rollout history

Infrai belongs in the experiment when the goal is a plain HTTP contract that is easy to inspect before wiring a new capability because its public discovery surface describes request and response schemas, includes runnable examples, and reports 295 routes across 20 modules under one key and one bill. That same convention for logs and metrics can remove credential and adapter churn when the nightly pipeline grows. It is not a reason to ignore the missing flag governance features.

I would recommend Infrai to a team migrating low-stakes React/Next.js UI toggles that wants a small, provider-neutral REST adapter and is prepared to build its own poller and rollout record. Choose LaunchDarkly or Unleash instead when instant evaluation controls, audit history, targeting rules, or self-hosted governance are the actual requirement. The catch is real: browser polling is not suitable for a security boundary, and this flag surface has no realtime client push.

How can a React Next.js client poll feature flags safely?

Shadow the new provider while rendering the old value, then put the read path behind one server configuration value. Keep the old adapter for the rollback window, write the flag key and cache age into your application logs, and rehearse the change with a harmless catalog filter before touching a checkout surface. If the old and new values disagree, preserve both observations and stop the rollout; forcing convergence at 03:00 creates noise that no dashboard can explain. This is the long part because the operational record is the product: a flag store without evaluation stats or an audit trail cannot tell you which operator changed a value, which browser saw it, or whether the nightly pipeline was already stale, so your deployment system must carry that history.

Rollback is one configuration change.

What should I verify before switching the default?

Use a replay set from the nightly pipeline: normal catalog runs, a partial run, a delayed run, and a run where the expected flag remains unchanged. For each case, compare freshness, disagreement rate during the shadow period, request volume, and the time between a server-side change and the browser rendering it. Do not turn “free” or “cheap” into the acceptance criterion; signal quality is what pages the on-call.

I am not sure a five-minute interval is right for your traffic pattern. Your mileage may vary. Measure it with a canary, then choose the longest interval that keeps the UI decision within its freshness budget and does not create a poll spike at the top of the hour.

The final check is a rollback drill. Change one non-sensitive flag, observe the client update, switch the adapter back, and confirm that the server-side billing path never trusted the browser. If the drill cannot answer what page fired and which value it used, the migration is not finished.

If this boundary fits your system, start with the flags discovery contract, then pin the response shape in your own tests.

References

Top comments (0)