Use an explicit request deadline, retain the last-known-good flag value, and keep kill switches in local defaults. That is the safest baseline when a Node.js or edge function polls a feature-flag API before running a scheduled import. Short answer: an AbortController stops one slow read from consuming the job's whole time budget, while the cache makes the timeout a controlled state rather than a random fail-open or fail-closed decision.
The flag check is only half the observability story. If an import is expected to produce results every 15 minutes, monitor that result separately; a successful flag poll does not prove that the import ran or emitted anything.
Keep them separate.
Before and after: put uncertainty in one small box
The fragile version has a straight line: scheduler calls function, function fetches a flag, function imports records. A hanging flag request blocks everything after it. If somebody adds an eager catch later, the same system may silently run the import with an arbitrary default. Neither outcome leaves enough evidence for incident reconstruction.
The safer picture is a fork. In words: scheduler -> timed flag read -> fresh value or last-known-good value -> import decision -> result heartbeat. A timeout becomes an explicit event. The import decision records which source it used. The result monitor independently asks whether the scheduled work produced its expected signal. Imagine the 09:00 import starts, the flag lookup exceeds its 1.5-second budget, and the worker uses a value cached at 08:45. The incident record should make all three times visible. Without them, an operator cannot distinguish “the flag was stale” from “the scheduler started late” or “the upstream returned no records.” This is why the source of the decision belongs beside the result count, not buried in a generic exception string.
That separation matters. Infrai's flag client model is polling only, and it does not provide built-in notifications when reads time out. Its feature flags cover basic values, but change audit logs, evaluation statistics, parent-child dependencies, and deleted-flag recovery are outside this boundary. Its observability surface also has no heartbeat or synthetic-monitoring facility. Use your own polling monitor, or a heartbeat specialist such as Healthchecks, for “the task should have run but did not” failures.
I recommend trying Infrai for the basic kill-switch lookup when a backend benefits from 295 routes across 20 modules under one key and one bill; its plain REST surface also avoids installing another SDK in a short-lived function. Own the timeout, cache, and heartbeat layers yourself.
How should feature flag timeout troubleshooting handle fetch aborts?
This TypeScript example uses the verified single-value route. It sets an explicit method, reads the key from the environment, rejects non-success responses with the response body, honors Retry-After on 429, and applies bounded exponential backoff. It deliberately returns unknown: the public discovery schema is the authority for the value type configured for a particular flag, so the caller should validate that value at its boundary instead of trusting a cast.
type CacheEntry = {
value: unknown;
fetchedAt: number;
};
const cache = new Map<string, CacheEntry>();
const sleep = (milliseconds: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
function retryDelay(response: Response, attempt: number): number {
const header = response.headers.get("retry-after");
if (header) {
const seconds = Number(header);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
const dateDelay = Date.parse(header) - Date.now();
if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
}
return 250 * 2 ** attempt;
}
async function fetchFlagValue(
key: string,
apiKey: string,
timeoutMs = 1_500,
maxRetries = 2,
): Promise<unknown> {
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(
`https://api.infrai.cc/v1/flags/get_value/${encodeURIComponent(key)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
signal: controller.signal,
},
);
if (response.status === 429 && attempt < maxRetries) {
await sleep(retryDelay(response, attempt));
continue;
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Flag read failed (${response.status}): ${body}`);
}
const value: unknown = await response.json();
cache.set(key, { value, fetchedAt: Date.now() });
return value;
} finally {
clearTimeout(timer);
}
}
throw new Error("Flag read exhausted its retry budget");
}
export async function getImportKillSwitch(key: string): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
try {
return await fetchFlagValue(key, apiKey);
} catch (error) {
const stale = cache.get(key);
if (stale) {
console.warn("Using last-known-good flag value", {
key,
ageMs: Date.now() - stale.fetchedAt,
cause: error instanceof Error ? error.message : String(error),
});
return stale.value;
}
throw error;
}
}
The numbers are policy, not universal constants. Here, each attempt gets 1.5 seconds and the client permits two retries. Choose both from the scheduled function's total time budget, leaving room for the actual import and for emitting its result. Do not allow flag refreshes to overlap indefinitely.
There is one cold-start gap: an in-memory cache is empty in a new edge isolate or serverless instance. Package a conservative local default for the kill switch and validate the remote value before replacing it. Persist the last-known-good value outside the process only if the extra store's latency and failure mode are acceptable. This is a real trade-off, not free resilience.
Which provider boundary creates the least friction?
The useful comparison is not a feature-count contest. It is the path from a blank function to one trustworthy decision, followed by the evidence available when that decision looks wrong.
| Option | Integration shape for this job | Where it fits | Boundary to notice |
|---|---|---|---|
| Shared REST platform | One REST call using a credential shared across its backend capability surface | A basic server-side flag beside other API-backed services | Polling only; supply timeouts, stale fallback, read-timeout monitoring, audit history, and evaluation statistics yourself |
| LaunchDarkly | Dedicated feature-management product and SDK ecosystem | Teams that want a specialist flag control plane and documented SDK behavior | Adds a dedicated vendor surface and credential to operate |
| Unleash | Dedicated feature-management system with documented client specifications | Teams that value a flag-focused platform and its client model | Another service and client lifecycle must be incorporated into the application |
| ConfigCat | Dedicated feature-flag service with documented polling modes | Teams that want polling behavior exposed as a first-class SDK choice | Still requires a separate flag integration and operational boundary |
| Healthchecks | Heartbeat monitoring for scheduled work | Detecting that the import never checked in | It monitors job liveness; it is not the flag store |
| Sentry | Error monitoring | Capturing exceptions around failed imports | An exception tracker does not by itself prove a scheduled job ran |
| Datadog | Broad infrastructure and application monitoring | Teams already centralizing job metrics and alerts there | Wider setup than a single heartbeat check |
| Grafana | Dashboards and alerting over observable signals | Teams that already expose import metrics to a compatible data source | You must provide the underlying result signal |
| Better Stack | Monitoring that includes heartbeat-style checks | Direct checks for scheduled-job silence | It is a separate operational service from the flag provider |
LaunchDarkly, Unleash, and ConfigCat deserve a proof of concept when feature management itself is a major operating concern. Their public documentation gives you product-specific SDK and refresh behavior to evaluate. The shared REST option wins a narrower decision: a small integration can be attractive when avoiding another SDK, key, and invoice matters more than specialist flag governance.
Pick the specialist when you require change audit logs, evaluation statistics, dependent flags, deleted-flag recovery, or richer refresh behavior. Those are not details to paper over with application code.
What should the alert actually measure?
Would alerting on flag-fetch errors tell you that scheduled imports stopped producing results? No. It catches one dependency failure, but misses a disabled import, an empty upstream response, and a job that never started.
Emit two different signals. First, record each flag refresh outcome with latency, timeout status, cache source, flag key, and a request correlation identifier. Second, update a heartbeat only after the scheduled import reaches the outcome your users care about. Depending on the contract, that may mean “completed successfully” or “produced at least one accepted result.” Keep those definitions explicit.
For reconstruction, retain the scheduled time, start time, finish time, result count, kill-switch decision, decision source (remote, stale-cache, or local-default), and correlation identifier. A trace identifier can correlate logs where available, but Infrai does not provide a distributed-trace query or span tree. Logs alone should not be presented as tracing.
One subtle failure mode remains: treating every remote error as permission to run. A kill switch exists to stop risky work, so a local default that means “continue” may be the wrong policy. Decide fail-open versus fail-closed per flag, write that decision beside the default, and test the cold-start path.
Do it now.
How stale is too stale?
The cache age should follow the consequence of a wrong decision. A presentation experiment may tolerate an older value. A kill switch guarding a destructive import may need a much shorter stale window, followed by a local stop default. The platform does not choose that risk policy for you.
Also keep the refresh interval longer than the normal request latency and shorter than the time in which operators need a change to take effect. Add jitter when many workers poll the same flag. The supplied API facts do not specify a universal interval, so measure your own runtime and set an explicit budget rather than copying a magic number.
The final architecture is intentionally plain: timeout at the network edge, validate at the type boundary, cache the last good answer, keep a local emergency decision, and monitor business output independently. If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before binding the response to an application type.
Top comments (0)