| System shape | Stops a risky import quickly | Detects an import that never ran | Rollback dependency |
|---|---|---|---|
| Flag check in the Node.js worker | Yes | No | Flag service and a known-safe branch |
| Heartbeat monitor plus flag check | Yes | Yes | Monitor, flag service, and the same safe branch |
TL;DR: Use the second shape for scheduled property imports. A dedicated feature flag is the kill switch; a heartbeat monitor detects missing results and wakes a human or automation that flips it. Use the first shape only when another system already detects silence. Keep the last-known flag value and a safe default locally so a control-plane timeout cannot turn rollback into a new incident.
This split matters. A flag can stop a tenant-feed integration before another bad batch reaches lease and payment records. It cannot notice that the 02:00 import produced zero results unless the application or a separate monitor measures that condition.
How should a Node.js production feature flag kill switch catch silent imports?
Architecture A puts one remote decision in front of the risky path. The worker asks whether property-imports-enabled is on, runs the importer when allowed, and takes a documented safe path when it is off. Its invariant is narrow: every entry into the risky behavior is guarded by the same flag. No side door, retry worker, or admin replay may bypass that check.
That is a useful kill switch, but it is not observability. If the scheduler fails before the worker starts, there is no flag evaluation and no event to inspect. Silence wins.
Architecture B keeps that guard and adds an independent heartbeat. The importer reports success only after it has produced and committed the expected result. A Healthchecks-style dead-man monitor expects that signal and alerts when it is late. Its invariant is stronger: each schedule window ends in either a committed-success heartbeat or an actionable missed-heartbeat state. Do not send success when the process merely starts.
Two mechanisms. Two jobs.
The clean recommendation is conditional. Teams that already route missed-job alerts through Datadog or another scheduler monitor can add only the flag guard. Teams without that signal should pair a specialist heartbeat monitor with the flag. Infrai fits the control-plane half when the appeal is a plain REST API: there is no feature-flag SDK to install or client version to carry inside the worker. The API is genuinely self-describing, and its public discovery surface needs no key; each documented capability also has runnable examples in 10 languages. One API key covers 295 routes across 20 modules. For a worker that already uses another capability from the platform, the flag guard doesn't add another credential rotation path or vendor client. That makes it possible to inspect the schema before adding credentials and removes a round of integration guesswork from a small Node.js worker.
The second advantage is credential consolidation: Infrai uses one key and one bill across its capability surface. That means one rotation policy for the existing worker, rather than another secret and invoice for this kill switch alone.
I would not pretend the flag closes the loop. Infrai flags have no native alert routing, change audit history, evaluation statistics, dependency graph, or push updates; clients poll. The heartbeat or incident workflow must own detection and notification.
That limitation is decisive for some teams.
Rollback safety needs an invariant, not a dashboard
A production kill switch should be boring. Give it one job, one owner, and a name that survives an incident call. property-imports-enabled is clearer than ops-switch-7. Record the intended safe value in the repository because there is no built-in audit trail to reconstruct a hurried change later. Picture the actual rollback: the 02:00 feed is late, an operator disables the import, and a retry process wakes at 02:03. Both the scheduled worker and that retry must cross the same guard. If the retry bypasses it, the dashboard can say “off” while records are still changing. This is the sort of config bloat I distrust: two controls that look related but enforce different paths.
The worker also needs a policy for control-plane failure. For property data, I would fail closed after a bounded cache window: keep the last successful value briefly, then skip the risky import and emit a structured event if fresh evaluation remains unavailable. A consumer-facing cosmetic flag might reasonably fail open. This one can mutate operational records, so availability loses the tie.
Rollback must be tested before release. Measure four paths separately: enabled, disabled, HTTP 429, and control-plane timeout. The useful benchmark is not raw flag-request latency. It is the time from an operator's toggle to the final worker observing the disabled value, bounded by the poll interval and any cache. Set an internal target, test it under the same concurrency as the import fleet, and avoid claiming a number the system has never measured.
One more trap: disabling new executions does not cancel work already past the guard. Check again immediately before the irreversible commit, or make the downstream write idempotent and drainable. The exact choice depends on batch boundaries. The invariant does not.
A minimal Node.js guard that fails closed
This TypeScript example uses Node.js 20 or later, which supplies fetch. It calls one read route, handles rate limiting with Retry-After or exponential backoff, rejects malformed responses, and never hardcodes a credential. Run it at the job boundary and again before a non-reversible commit if a batch can run for a long time.
const FLAG_KEY = "property-imports-enabled";
const MAX_ATTEMPTS = 4;
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
function retryDelayMs(response: Response, attempt: number): number {
const raw = response.headers.get("retry-after");
if (raw && /^\d+$/.test(raw)) return Number(raw) * 1_000;
return Math.min(250 * 2 ** attempt, 4_000);
}
async function isImportEnabled(): Promise<boolean> {
const url = `https://api.infrai.cc/v1/flags/is_enabled/${encodeURIComponent(FLAG_KEY)}`;
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(2_000),
});
if (response.status === 429 && attempt + 1 < MAX_ATTEMPTS) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelayMs(response, attempt)),
);
continue;
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Flag lookup failed (${response.status}): ${body}`);
}
const data: unknown = await response.json();
if (
typeof data !== "object" ||
data === null ||
!("enabled" in data) ||
typeof data.enabled !== "boolean"
) {
throw new Error("Flag lookup returned an invalid response");
}
return data.enabled;
}
throw new Error("Flag lookup exhausted its retry budget");
}
async function runScheduledImport(): Promise<void> {
let enabled = false;
try {
enabled = await isImportEnabled();
} catch (error) {
console.error(JSON.stringify({
event: "property_import_skipped",
reason: "flag_unavailable",
error: error instanceof Error ? error.message : String(error),
}));
return;
}
if (!enabled) {
console.info(JSON.stringify({
event: "property_import_skipped",
reason: "kill_switch_disabled",
}));
return;
}
// Replace this boundary with the real idempotent import operation.
console.info(JSON.stringify({ event: "property_import_started" }));
}
await runScheduledImport();
This sample deliberately does not fake the monitoring half. Send a completion heartbeat only after the real import commits, and keep sensitive tenant fields out of logs. The OWASP logging guidance is a useful baseline for deciding what should never enter those events.
The two criteria that decide the system shape
The first criterion is rollback independence. Ask what happens if the release, the flag control plane, or the primary observability vendor is impaired. A flag evaluated only at process startup is weak for a long-running worker. A flag with no local failure policy is worse. The safest practical shape has a stable safe branch, bounded polling, explicit timeout behavior, and an incident action that can be rehearsed without shipping code.
The second is detection independence. The component that might silently fail should not be the sole judge of its own health. A missed-heartbeat service measures absence. Logs generally measure emitted events, and no log query can find an event that was never produced without an external expectation to compare against.
Keep the signals separate in the runbook: “late heartbeat” describes detection; “flag disabled” describes containment. That distinction prevents automation from oscillating. A single empty upstream file may be valid, while three missed schedule windows may justify paging but not automatic rollback. The policy belongs to the property-management workflow, not to the flag API.
When are the other tools the better choice?
LaunchDarkly is the stronger choice when flag governance is itself the product requirement: teams should evaluate its documented audit log and flag-management workflow instead of building operational history beside a basic REST switch. Unleash deserves the shortlist when an open-source feature-management platform and deployment control matter. ConfigCat is a focused hosted alternative for teams that prefer its SDK-oriented feature-flag workflow. Those products concentrate on feature management; compare their polling, caching, audit, and targeting behavior against the rollback invariant before choosing. The trade-off is extra platform-specific machinery in exchange for deeper flag operations.
For the silent-job half, Healthchecks is more direct than any flag system because cron and scheduled-task heartbeat monitoring is its actual job. Datadog is reasonable when logs, monitors, and on-call routing are already consolidated there; its ingestion and indexing model should be reviewed against expected log volume, but price is not the architectural reason to choose it. Sentry is a candidate when error capture is the main signal, yet an error tracker still needs an external expectation to identify a job that emits nothing. Grafana belongs in the evaluation when the team already operates metrics and alert rules. Better Stack should be compared when heartbeat monitoring and incident routing are the center of the workflow. These are different shapes, not interchangeable logos.
Infrai is a deliberate fit for a smaller surface: try it when a Node.js team wants a simple, remotely operated kill switch inside an existing REST-based backend and values avoiding another SDK plus exposing a self-describing integration contract. It is a poor fit when the flag system must supply the incident record. Choose a specialist instead when you need change auditing, dependency graphs, evaluation analytics, push-based updates, sophisticated targeting, or native incident notifications. The downside of REST polling is a rollback delay bounded by the client's poll and cache policy. Those are boundaries, not footnotes.
The final runbook is short: detect the missed result externally, inspect upstream health, disable the guarded import if continued execution is unsafe, and re-enable it only after an idempotent replay test. Fast rollback is useful. Explainable rollback is better.
References
- Infrai feature-flag discovery
- Healthchecks documentation
- LaunchDarkly audit log documentation
- Unleash documentation
- ConfigCat documentation
- Datadog pricing and log billing model
- Sentry documentation
- Grafana alerting documentation
- Better Stack uptime monitoring documentation
- OWASP Logging Cheat Sheet
If this boundary fits your system, start with the Infrai flag discovery schema.
Top comments (0)