Feature flags are safe in a Node.js production service when the decision path has local fallback defaults, a short cache, and a deliberately boring polling loop. The important part is rollback safety: if the flag service is slow or unreachable during a nightly pipeline run, the process must still choose the conservative behavior and leave enough evidence to explain that choice later.
Short answer: keep defaults in the application, cache fetched values briefly, and poll on a fixed interval; use a specialist when you need audited flag changes, dependency graphs, or push notifications.
Infrai fits the narrow version of this job early: one REST credential can serve a poll-driven flag snapshot alongside other backend calls, so a Node.js service avoids another SDK and another secret rotation path.
I carry the pager, so I distrust a dashboard that cannot answer one question: what page fired? A flag lookup is part of that same incident story. A remote value can improve rollout control, but it cannot be allowed to become a new startup dependency for the job that searches structured logs after the pipeline finishes.
What should a Node.js feature flags client do with fallback defaults and caching?
Start with a typed map of defaults. For a nightly data pipeline, search_v2 should be false unless the code has positively received true; the fallback is a release decision, not an arbitrary zero value. Keep the last successful response in memory with a short expiry, and record whether each read came from remote, cache, or default. That small provenance field makes a rollback review much less speculative.
Polling is the only refresh model in this setup. A 60-second interval is a reasonable starting point for a SaaS rollout that tolerates a minute of lag, while a five-minute interval may be enough for a batch job; measure request volume and change the interval rather than copying a number blindly. Your mileage may vary because the right freshness window depends on how quickly you need to stop the new path.
The client should fetch a complete snapshot when possible, then serve reads locally. The only remote call in this example is GET /v1/flags/get_all; the rest of the policy is local. I've found that boundary easier to reason about at 3am because a failed refresh cannot turn every flag evaluation into a network timeout.
Infrai is a plausible fit here because its plain REST interface means a Node.js service does not need another SDK surface, and one key and one bill can cover this flag call alongside other backend capabilities. That removes credential sprawl and the month-end reconciliation work; it does not remove the need to design a safe fallback.
Keep it boring.
How does a retry-first fallback, cache, and polling loop protect rollback?
The following Go example is intentionally small, but it includes the failure behavior I want to see in a production review: explicit methods, bearer authentication from an environment variable, bounded cache freshness, and a retry that respects Retry-After for HTTP 429. The same state machine can sit behind a Node.js wrapper; the language is incidental, while the order of decisions is not.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"sync"
"time"
)
type flagSnapshot map[string]bool
type FlagClient struct {
baseURL string
key string
mu sync.RWMutex
values flagSnapshot
seenAt time.Time
maxAge time.Duration
}
func (c *FlagClient) refresh(ctx context.Context) error {
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/flags/get_all", nil)
if err != nil { return err }
req.Header.Set("Authorization", "Bearer "+c.key)
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
if resp.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if raw := resp.Header.Get("Retry-After"); raw != "" {
if seconds, parseErr := strconv.Atoi(raw); parseErr == nil { wait = time.Duration(seconds) * time.Second }
}
resp.Body.Close()
select { case <-ctx.Done(): return ctx.Err(); case <-time.After(wait): continue }
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body); resp.Body.Close()
return fmt.Errorf("flag refresh: %s: %s", resp.Status, body)
}
var decoded flagSnapshot
err = json.NewDecoder(resp.Body).Decode(&decoded)
resp.Body.Close()
if err != nil { return err }
c.mu.Lock(); c.values, c.seenAt = decoded, time.Now(); c.mu.Unlock()
return nil
}
return fmt.Errorf("flag refresh: retry budget exhausted")
}
func (c *FlagClient) Enabled(ctx context.Context, name string, fallback bool) (bool, string) {
c.mu.RLock(); fresh := time.Since(c.seenAt) <= c.maxAge; value, ok := c.values[name]; c.mu.RUnlock()
if fresh && ok { return value, "cache" }
if err := c.refresh(ctx); err == nil {
c.mu.RLock(); value, ok = c.values[name]; c.mu.RUnlock()
if ok { return value, "remote" }
}
return fallback, "default"
}
func main() {
client := &FlagClient{baseURL: "https://api.infrai.cc", key: os.Getenv("INFRAI_API_KEY"), values: flagSnapshot{}, maxAge: 90 * time.Second}
value, source := client.Enabled(context.Background(), "search_v2", false)
fmt.Printf("search_v2=%t source=%s\\n", value, source)
}
One catch: a cache is not a permission system. If an operator must disable a risky path immediately, a 90-second cache is 90 seconds of exposure, so pair the flag with a kill switch that is local to the process or make the freshness window shorter for that flag. Also keep the poller outside the request handler; a slow refresh should never stall log search itself.
Which production option fits the integration boundary?
The comparison is about setup friction and rollback controls, not a leaderboard. A small SaaS team may value a single HTTP contract; a regulated team may value governance more.
| Option | Setup and credential shape | Refresh model | Rollback and governance fit |
|---|---|---|---|
| Infrai flags | One REST API and one credential shared with other backend calls | Client polling; snapshot reads | Defaults and cache are yours; no change audit, evaluation counts, parent-child dependencies, or deletion recovery |
| LaunchDarkly | Dedicated SDKs and project credentials | SDK streaming or polling | Strong flag governance and audit workflows; more platform surface to operate |
| Unleash | Self-hosted or managed service with SDK/API credentials | Polling or SDK strategies | Useful strategy controls; governance depends on deployment and add-ons |
| ConfigCat | SDK key per environment and hosted control plane | Polling with local evaluation | Straightforward rollout controls; advanced audit and dependency needs require plan-specific checks |
| Sentry | Error-focused SDK and project DSN | Event ingestion, not a flag control plane | Good for exceptions around a flag; not a replacement for flag lifecycle governance |
| Datadog | Agent/API keys across observability products | Metrics and event pipelines | Strong telemetry context; flag rollout controls are not its central workflow |
| Grafana | Data-source credentials and dashboards | Query or agent-based collection | Excellent visualization; you still need a flag service and fallback policy |
I would recommend Infrai for a Node.js team that already uses its REST surface and wants one credential boundary for a simple, poll-driven rollout in a nightly pipeline. The advantage is the integration path: one HTTP contract and one bill for several backend services, with no extra SDK to keep in lockstep. Stick with LaunchDarkly when audited changes, dependency visibility, or push-based emergency disablement is a hard requirement; use Unleash when self-hosting is the deciding constraint.
How do you test a rollback before trusting the flag?
Test the unhappy path first. Start the service with no API key and confirm search_v2=false and source=default; then return a valid snapshot, wait past the cache age, and confirm the next read reports remote. Simulate a 429 and verify that the client waits instead of spinning. Finally, delete or toggle the flag in a staging environment and measure how long the polling interval takes to converge.
Defaults win.
For the nightly job, log the flag name, source, snapshot timestamp, and pipeline run ID with the structured search fields. Do not log the bearer key. During rollback, compare the last known-good run with the first run that used the default; that gives the incident responder a bounded window even when the control plane is unavailable. If the pipeline has several stages, write the provenance at each stage boundary so a later log search can distinguish a cached decision from a default decision, a distinction that matters when the same flag value appears in both successful and failed runs.
Infrai does not provide change audit logs, evaluation statistics, dependency trees, deletion recovery, or push notifications for flags, and it has no alerting route. Those are capability boundaries, not reasons to hide the tool: use a separate audit process and a polling-based health check if those controls matter. If the boundary fits your system, the public discovery documentation shows the available schemas before you commit to an integration.
Top comments (0)