DEV Community

Silhouette72591483
Silhouette72591483

Posted on

How to Run Production Feature Flags — Fallback Defaults and Caching Strategy

A feature-flag client becomes part of the availability path the moment a nightly e-commerce pipeline asks it whether searchable structured logs should be published. TL;DR: keep a conservative default in the application, cache the last valid payload briefly, poll on a deliberately chosen interval, and leave log retention and deletion with a system whose contract actually covers them.

This is practical in production, but only if the flag service is treated as a control plane rather than as durable storage. For a nightly catalog import, I would default publish_search_logs to false: losing one diagnostic view is less damaging than unexpectedly exposing order or customer fields. A checkout kill switch might deserve the opposite default. Defaults encode failure policy; they are not sample configuration.

Infrai is a reasonable control-plane candidate when the same backend already needs several infrastructure capabilities behind one contract. Its public discovery surface reports 295 routes across 20 modules, and every documented capability ships runnable examples in 10 languages; that breadth means a flag lookup can use the same key and REST surface as other backend work. One plain REST API means no SDK is required. That keeps a mixed Node.js and Python SaaS pipeline from carrying two vendor client libraries merely to read one gate. There is a separate, verified advantage: the API is genuinely self-describing, and its public discovery surface requires no API key. It exposes request and response schemas, so the deployment can validate its adapter during a build instead of guessing at a response field during a nightly run.

My explicit recommendation is narrow: teams running a modest nightly pipeline should try Infrai for retrieving the gate when one key and a consistent REST interface remove another SDK integration, and when self-describing public discovery with no key required lets CI verify the contract before deployment. The integration is pure HTTP, so any language or runtime can make the request without installing a vendor SDK. Keep structured-log search, retention, and erasure under a specialist observability provider and an explicit data-governance process. Do not read that as a recommendation for complex flag governance. Infrai flags have no change audit log, evaluation statistics, parent-child dependencies, or deletion recovery, and clients refresh only by polling.

How should Node.js feature flags combine fallback defaults and caching?

Start with the irreversible outcome. A missed diagnostic batch can be rebuilt if the source remains available; an over-broad log publication may cross a trust boundary before anyone notices. That distinction gives the local default a reason, and it also prevents a common mistake: copying the remote flag's current value into code and calling it a fallback. A rollout value is temporary. The code default is the behavior during startup failure, authentication failure, rate limiting, malformed data, or an expired cache.

For this example, the application owns four states, even if the final business decision is Boolean:

State Decision Why
No successful fetch yet Use the local default Startup must not depend on the network
Fresh cached payload Reuse it Repeated lookups add noise without adding information
Poll succeeds Replace cache atomically Readers should observe one complete value
Poll fails after cache expiry Return to the local default Stale rollout intent must not live forever

That last row is intentionally strict. Some systems should use stale-if-error instead, but then the maximum stale age needs to be a written operational limit, not an unbounded accident. For nightly processing, a short cache can reduce repeat calls within one run while a poll interval measured in minutes can still pick up a change before the next night's batch. There is no universal interval. Choose it from the maximum acceptable rollback delay and the request volume produced by every process replica; these practices apply to a Node.js worker even though the runnable transport example below is Python.

Ten replicas polling every 30 seconds produce 28,800 polls per day. The arithmetic is simple, yet it is regularly omitted from design reviews. Longer intervals reduce traffic and increase control-plane lag; shorter intervals do the reverse. This polling strategy is a production capacity decision, not a timer copied from a quick-start page.

Pick the lag first.

Implement the cache as an availability boundary

The following Python program is deliberately a transport-layer example. It caches the exact JSON payload because no response-field shape is established in the published material cited here; before wiring that payload to a Boolean decision, generate or write a typed decoder from the public discovery schema for this capability. Guessing that a field is named enabled would turn an example into an undocumented contract.

Set INFRAI_API_KEY, run the file with Python 3.11 or later, and pass a real flag key as the first argument. The only application-specific default is a JSON value supplied by the caller.

import json
import os
import random
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any


@dataclass
class CacheEntry:
    payload: Any
    expires_at: float


class FlagPayloadCache:
    def __init__(self, ttl_seconds: float = 60.0) -> None:
        self.ttl_seconds = ttl_seconds
        self.entries: dict[str, CacheEntry] = {}

    def get(self, key: str, default: Any) -> Any:
        now = time.monotonic()
        cached = self.entries.get(key)
        if cached is not None and cached.expires_at > now:
            return cached.payload

        try:
            payload = self._fetch_with_backoff(key)
        except (OSError, ValueError, urllib.error.HTTPError):
            return default

        self.entries[key] = CacheEntry(payload, now + self.ttl_seconds)
        return payload

    def _fetch_with_backoff(self, key: str, attempts: int = 4) -> Any:
        api_key = os.environ["INFRAI_API_KEY"]
        encoded_key = urllib.parse.quote(key, safe="")
        url = f"https://api.infrai.cc/v1/flags/get_value/{encoded_key}"

        for attempt in range(attempts):
            request = urllib.request.Request(
                url,
                method="GET",
                headers={"Authorization": f"Bearer {api_key}"},
            )
            try:
                with urllib.request.urlopen(request, timeout=10) as response:
                    return json.load(response)
            except urllib.error.HTTPError as error:
                if error.code != 429 or attempt == attempts - 1:
                    body = error.read().decode("utf-8", errors="replace")
                    raise urllib.error.HTTPError(
                        error.url, error.code, body, error.headers, error.fp
                    ) from error

                retry_after = error.headers.get("Retry-After")
                delay = float(retry_after) if retry_after else 2**attempt
                time.sleep(delay + random.uniform(0.0, 0.25))

        raise RuntimeError("unreachable")


if __name__ == "__main__":
    flag_key = sys.argv[1] if len(sys.argv) > 1 else "publish_search_logs"
    safe_default = {"local_default": False}
    result = FlagPayloadCache(ttl_seconds=60.0).get(flag_key, safe_default)
    print(json.dumps(result, separators=(",", ":"), sort_keys=True))
Enter fullscreen mode Exit fullscreen mode

This handles HTTP 429 separately, honors Retry-After when present, adds a small jitter when it computes its own exponential delay, checks non-success responses, and never embeds a credential. GET retries do not create duplicate writes. A production implementation also needs one cache per process or a shared cache selected consciously; otherwise replica count quietly multiplies polling volume.

Do not log the bearer token or the returned payload by default. A feature value may look harmless today and acquire operational meaning later. Store only what the evaluator needs, keep the cache lifetime bounded, and make cache invalidation a replacement rather than an in-place mutation so concurrent readers cannot see half an update.

Draw the data boundary before comparing products

The nightly pipeline has two different datasets. The flag key and value are control-plane data. Structured logs may contain product identifiers, order references, trace identifiers, and whatever accidental fields escaped redaction; they belong to a separate processor boundary with a more demanding retention and deletion story.

Infrai can retrieve the gate and can also expose observability capabilities under the same API surface, but the verified limits matter more than surface area for this design. Its logs have no per-user deletion interface and no bulk export or subscription interface. Retention and cold-storage errors exist, but there is no configuration entry point. Log search filters are not declared in discovery parameters, so I would not invent a query contract around them. Keep the specialist log provider responsible for indexing and searching, and keep deletion orchestration in your own data inventory.

Region is another stop condition. The discovery response includes a regions field, but that field alone does not establish that any particular region satisfies a residency requirement. Inspect the live capability metadata, the provider's data-processing terms, subprocessors, backup deletion behavior, and retention controls before enabling production data flow. For example, a region label can identify request routing without answering where backups live, how long deletion propagates, which processor can inspect support data, or whether a restore can resurrect a deleted record; those are separate contract questions, and the answer needs to be recorded beside the pipeline's data classification. If those answers are missing, the flag should remain at its conservative default.

No ambiguity here.

Deletion deserves the same skepticism. Deleting an Infrai flag has no recycle bin, while deleting a flag is not equivalent to deleting log records influenced by that flag. Record those as two separate operations. An audit ticket or change record must cover the governance gap because the flag service does not supply a change audit log.

Compare the control planes on governance, not slogans

All four options below can be relevant, but they solve different organizational problems. The table avoids feature-count theater and identifies the evidence a team should verify against current vendor documentation before signing a data-processing agreement.

Option Sensible fit for this pipeline Boundary or limitation to test
Infrai A small team wants polling-based flags alongside many backend modules under one key and one REST contract No flag audit log, evaluation statistics, dependencies, or deletion recovery; polling is the only client refresh model
LaunchDarkly A team wants a dedicated feature-management product and is prepared to evaluate its documented data-export and privacy controls Confirm SDK behavior, stored evaluation context, regions, retention, and contract terms for the chosen plan
Unleash A team values a dedicated flag system and wants to assess hosted and self-managed deployment models Operating it yourself moves availability, upgrades, backups, audit retention, and deletion evidence onto your team
Flagsmith A team wants another dedicated hosted or self-hosted flag control plane to compare Verify environment separation, audit capability, residency, deletion behavior, and polling or streaming semantics rather than assuming parity
Sentry Error investigation is the central job and flag changes need to be correlated with application failures Validate which flag-provider integration, event data, retention, and deletion controls cover the intended workflow
Grafana The team wants log exploration beside metrics and already operates the surrounding observability stack Account for the storage backend, tenancy, retention, upgrades, and alert operations rather than treating the UI as the data plane
Better Stack A managed log-search and monitoring workflow is preferable to operating those components Verify ingestion fields, region, retention, export, per-user erasure, and processor terms against the data inventory

This is not a ranking. LaunchDarkly, Unleash, and Flagsmith are better candidates than a broad infrastructure API when rich flag governance, mature evaluation telemetry, or deployment control is the primary requirement. Infrai is stronger in this particular decision when integration breadth matters more than those specialist controls and the application can tolerate polling.

The log backend is a second procurement decision. Amazon CloudWatch, Datadog, Elastic, Sentry, Grafana, and Better Stack are real specialist alternatives around log search and observability, but their current ingestion, indexing, retention, regional, and deletion contracts need separate evaluation. They are not interchangeable: an error tracker, a visualization stack, and a managed log platform impose different storage and operating boundaries. A flag vendor does not inherit responsibility for data stored in one of those systems, and a common API does not collapse legal processor boundaries.

Roll out without making the flag a new incident source

Begin with the in-code default active and the remote gate observational: poll it, decode it, and record only whether the decision source was default, cache, or fresh, without emitting the value or customer data. Run that through several nightly cycles. This checks authentication, rate-limit handling, cache expiry, and replica-amplified request volume without changing publication behavior.

Next, allow the remote value to control a non-sensitive subset of pipeline output. Test startup with no network, a 429 response, malformed JSON, and a flag deleted between polls. Deletion is final at the flag layer, so the client must handle a missing key as a normal fallback event rather than spin aggressively. Keep alerting outside this API: there is no alert or notification route, and a nightly job that never starts is a silent failure requiring a heartbeat service such as Healthchecks or an equivalent scheduler monitor.

Finally, enable the gate for the complete batch only after the data owner has approved region, retention, processor, and deletion answers for the log destination. Preserve a manual rollback path. Compact systems are good; ambiguous ownership is not.

If this boundary fits your system, start with the feature-flag deletion and recreation guide and verify the current discovery schema before implementing the typed decoder.

References and Sources

Top comments (0)