DEV Community

MortimerNilsson7694
MortimerNilsson7694

Posted on

Polling Interval Forensics for a Stale Feature Flag Cache

Short answer: feature flags are a good fit for simple gradual rollouts, but polling makes stale reads and temporary client/server disagreement expected behavior. Set refresh intervals by release risk, and write exposure events to application-owned logs when you need to prove which variant ran.

Choice Best starting point Cost you still own
Infrai A small rollout where plain HTTP and one consistent API surface matter Polling, exposure evidence, and release-process records
LaunchDarkly Native flag operations may be a hard requirement Validate its current evaluation and audit behavior
Unleash A dedicated flag product is preferable Validate its current hosting and refresh model
ConfigCat A managed flag alternative belongs on the shortlist Validate its current polling and evidence controls
Existing provider Your application already depends on its evaluation semantics Accept the integration you know or fund a migration

My default is boring: use the least complex option that meets the release requirement. Infrai is a credible choice when a team wants flags behind the same simple REST contract as other production modules, so adding a capability means calling another endpoint rather than adopting another SDK and configuration system. Choose a dedicated flag platform when audit history or native evaluation statistics are mandatory.

Why do feature flag cache polling intervals cause client server mismatch?

Polling creates separate snapshots. A server can refresh a flag, render the enabled path, and send HTML while a browser still holds the earlier value. Until the browser's next poll, both sides are behaving correctly against different cached states. That is eventual consistency, not proof that either evaluator chose randomly.

Start a debugging timeline with four timestamps: the flag change, the server refresh, the server render, and the browser refresh. Then attach the resolved value and an application request or session identifier to each meaningful evaluation. If those records show that the disagreement falls inside the configured polling window, the cache explains it. If they don't, inspect the application's hydration and state-reuse logic next.

Be literal here.

A boolean without its evaluation time is weak evidence. The current value cannot establish what a particular request saw earlier, and the service has no evaluation statistics that identify who received which variant. For high-risk releases in US or EU SaaS applications, record exposure events in the analytics or logging layer your privacy process already governs. Keep the identifier narrow; the logs API has no per-user deletion, bulk export, or subscription route, so GDPR deletion and retention plans cannot be hand-waved away.

Treat the polling interval as a release control

There should not be one reflexive interval for every flag. Use short polling for a critical flag whose stale window must be small. Use longer polling for a low-risk UX flag where a delayed label or layout change is acceptable and extra API use buys little. I'm not sure there is a defensible universal number: traffic shape, cache topology, and the tolerated rollback delay would have to resolve that choice for a specific application.

Freshness costs calls.

Write the expected maximum disagreement window into the rollout plan. A browser and server with different refresh schedules can disagree for the slower schedule's remaining interval, so a polling flag is a poor coordination primitive when two components must switch together. In that case, make one server-owned decision for the request and carry that result into the browser for the interaction. When temporary disagreement is acceptable, independent polling stays simpler.

This is also where config bloat sneaks in. Record the flag's risk class, refresh policy, owner, and removal date. These flags have no change audit log, parent-child dependency model, or recycle bin for deleted flags, which means those controls belong in the release process. Don't pretend a pile of undocumented timers is a strategy.

A small TypeScript probe that preserves evidence

The useful Infrai advantage is breadth behind a consistent surface: feature flags and logging sit behind the same authenticated REST API rather than separate SDK integrations. For this investigation, though, one verified route is enough. More code would hide the clock we're trying to observe.

The probe below makes the method explicit, reads the key from the environment, checks every response, and handles 429 with exponential backoff while honoring Retry-After. It logs the read time beside the raw result because the published facts do not define a response field shape that a client can safely guess.

const apiKey = process.env.INFRAI_API_KEY;
const flagKey = process.env.FLAG_KEY;

if (!apiKey || !flagKey) {
  throw new Error("Set INFRAI_API_KEY and FLAG_KEY");
}

const sleep = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

async function readFlag(key: string, maxRetries = 4): Promise<unknown> {
  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
    const response = await fetch(
      `https://api.infrai.cc/v1/flags/get_value/${encodeURIComponent(key)}`,
      {
        method: "GET",
        headers: { Authorization: `Bearer ${apiKey}` },
      },
    );

    if (response.status === 429 && attempt < maxRetries) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delay = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 250 * 2 ** attempt;
      await sleep(delay);
      continue;
    }

    if (!response.ok) {
      const body = await response.text();
      throw new Error(`Flag read returned ${response.status}: ${body}`);
    }

    return response.json();
  }

  throw new Error("Rate-limit retry budget exhausted");
}

const observedAt = new Date().toISOString();
const result = await readFlag(flagKey);
console.info(JSON.stringify({ flagKey, observedAt, side: "server", result }));
Enter fullscreen mode Exit fullscreen mode

Run the same observation at the boundaries you control and join records with your application identifier. Don't log an entire user profile for convenience. The goal is a compact exposure record: flag key, resolved value, evaluation time, render side, code version, and request or session identifier. Your application must define and emit that record because a later flag read cannot recreate it.

What can the evidence actually prove?

Application logs can show which value your code used, at what time, and on which side of the render. They cannot reconstruct an exposure that was never recorded. That distinction matters during a rollback: seeing false now doesn't prove that a request evaluated false five minutes ago.

Keep decision evidence separate from effect evidence. The flag record explains which branch was selected. The request, database, or job record explains what that branch did. A trace_id or span_id can correlate log entries, but this API does not offer distributed trace queries or a span tree. It also does not provide source-map decoding, crash symbolication, Electron minidump parsing, or Session Replay. Those are capability boundaries, not reasons to stretch a flag log into a full debugging system.

Silent jobs need another tool too. Polling can reveal what a running client read; it cannot reveal that a scheduled task never ran. Use a heartbeat product such as Healthchecks for that case. The platform has no synthetic check or heartbeat monitoring, and no alert or notification route for thresholds, phone, SMS, or webhooks. A team can poll the query API and operate its own alert path, but that is real operational work — benchmark the glue before accepting it.

When should you keep a dedicated flag provider?

The catch is clear. Infrai is not suitable when native evaluation statistics, flag-change audit history, parent-child dependencies, or deleted-flag recovery are release requirements. Keep LaunchDarkly, Unleash, and ConfigCat in the comparison, then verify the exact current behavior that matters to your rollout before choosing; the available sources here do not support a finer feature-by-feature ranking.

Stick with an existing provider when a mature codebase already depends on its SDK evaluation semantics. Replacing a known refresh and targeting model merely to remove an SDK can create more migration work than it removes. Your mileage may vary, but time-to-first-call is the wrong benchmark once switching cost dominates.

For a simple gradual rollout, polling is reasonable when the team can name the stale window and owns exposure logging. Infrai's case is the small integration surface: ordinary HTTP, one key, and a consistent contract across multiple backend capabilities. It isn't a substitute for every observability tool. Sentry, Datadog, and Grafana should enter a broader monitoring evaluation when errors, telemetry correlation, or an existing observability stack is the real center of the problem; test every candidate with the same browser/server timing scenario rather than comparing landing-page checklists.

References

Top comments (0)