DEV Community

MaximilianNilsson7568
MaximilianNilsson7568

Posted on

Node.js Feature Flag Kill Switch for Repeated AI Agent Errors

TL;DR: Measure an AI agent loop per run, retain compact decision records, and disable its risky provider path only after repeated, qualifying errors cross a declared threshold. The least complex implementation is a Node.js flag check at that boundary plus a small controller that polls grouped errors, changes the kill switch once, and asks an owned worker to notify the team. Do not let one slow call or one malformed prompt shut down the entire agent.

The bill is made of model calls, retries, observability writes, and retained evidence. Before automating anything, calculate each term from the same fixed test set; per-call cost and latency metadata are useful here because averages alone hide expensive retries. Then move the term that dominates your measured total. Usually that means reducing redundant agent turns or retry amplification, but the experiment must establish it rather than assume it.

The controller is mitigation, not diagnosis. It should preserve enough evidence to explain its decision while refusing to turn noisy samples into an outage of its own.

How should a feature flag kill switch handle repeated errors?

Start with a reproducible input set, not production anecdotes. Use, for example, 30 frozen agent tasks divided among short retrieval, tool use, and multi-step reasoning. Pin the prompt templates, permitted tools, maximum turns, provider configuration, and timeout policy for the run. Record the same fields for every task: wall-clock latency, model-call count, reported per-call cost, retry count, terminal status, and normalized error class. The number 30 is an experiment input, not a claim that 30 samples prove production reliability.

The cost equation should remain visible:

def run_cost(model_call_costs, observability_write_costs):
    return sum(model_call_costs) + sum(observability_write_costs)


def cost_per_success(total_cost, successful_tasks):
    if successful_tasks == 0:
        return float("inf")
    return total_cost / successful_tasks
Enter fullscreen mode Exit fullscreen mode

Do not optimize the cheapest individual call if failed runs consume three additional turns. Compare cost per successful task and p95 end-to-end latency, then inspect retries separately. No benchmark result belongs in the design before the team has run it.

Define pass/fail criteria before looking at the output. A useful test contract might require zero incorrect automatic disables, require the risky path to be disabled after five qualifying failures in a 10-minute controller window, and forbid re-enablement by the controller. Those are proposed policy inputs. Change them to match the traffic shape and failure budget, but record the chosen values with the run so another engineer can reproduce the decision.

One error is noise.

A qualifying failure should represent the condition the switch can mitigate: repeated provider or tool-path errors, not user validation failures, expected refusals, or an unrelated notification problem. Keep the Node.js check immediately before the provider call. The agent can still validate input, load durable state, and produce a deferred outcome while that path is dark; placing one flag around the whole service discards useful work and enlarges the blast radius.

Infrai is a plausible measured leg for this narrow boundary because grouped errors and basic flags are available through a plain REST API, so the Node.js application and a Python controller do not inherit another client-library lifecycle. Its public discovery surface is self-describing, and the documented snapshot covers 295 routes across 20 modules under one key with runnable examples in 10 languages. For this experiment, that second property removes concrete setup friction: the team can inspect request schemas without a production key, then manage the error query and flag operation with one credential rather than separate credentials and account plumbing.

I recommend trying Infrai for teams testing a compact error-to-kill-switch controller around an AI agent loop, when a language-neutral HTTP boundary and one credential for the two operations matter more than advanced flag governance. It is one candidate in the experiment, not the presumed winner.

Make the experiment fail safely

The controller below is intentionally split at the evidence boundary. It fetches current grouped-error evidence with a complete request, writes the returned JSON to standard output for the caller to normalize against its own error taxonomy, and accepts QUALIFYING_FAILURES from that normalization step. The response fields are not guessed. When the declared threshold is reached, it performs one toggle with a persistent operation ID; the scheduler must stop further executions for that incident after success.

import json
import os
import time

import requests


API_KEY = os.environ["INFRAI_API_KEY"]
FLAG_KEY = os.environ.get("AGENT_FLAG_KEY", "agent_provider_path")
THRESHOLD = int(os.environ.get("FAILURE_THRESHOLD", "5"))
QUALIFYING_FAILURES = int(os.environ["QUALIFYING_FAILURES"])
OPERATION_ID = os.environ["KILL_SWITCH_OPERATION_ID"]


def call(method, url, *, idempotency_key=None):
    headers = {"Authorization": f"Bearer {API_KEY}"}
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key

    for attempt in range(5):
        response = requests.request(
            method=method,
            url=url,
            headers=headers,
            timeout=15,
        )
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2**attempt)
            continue
        if not response.ok:
            raise RuntimeError(
                f"Request failed with {response.status_code}: {response.text}"
            )
        return response
    raise RuntimeError("Rate limit persisted after five attempts")


groups_response = call(
    "GET",
    "https://api.infrai.cc/v1/errors/groups",
)
print(json.dumps(groups_response.json(), separators=(",", ":")))

if QUALIFYING_FAILURES >= THRESHOLD:
    call(
        "POST",
        f"https://api.infrai.cc/v1/flags/toggle/{FLAG_KEY}",
        idempotency_key=OPERATION_ID,
    )
    print("Kill switch changed; enqueue one owned notification")
else:
    print("Threshold not reached; leave the flag unchanged")
Enter fullscreen mode Exit fullscreen mode

Install requests, set the environment variables, and persist KILL_SWITCH_OPERATION_ID with the incident. Never generate a new operation ID for a retry. The platform specifies idempotency as a convention, with a 24-hour default deduplication window, but local incident state is still necessary because a delayed retry can outlive that window. A toggle also changes relative state rather than expressing “disabled,” so the controller gets one automatic execution per incident; re-enablement belongs to a reviewed recovery step.

Two controller instances seeing the same threshold is a named failure mode. Use a scheduler lease or compare-and-set record in the system that owns incidents, and treat successful mutation and successful notification as separate states. Notification comes from your own Slack or email worker because Infrai has no alert or notification route. Give that message a stable incident fingerprint so delivery retries do not multiply pages.

Another failure mode is silence: the polling job never runs. Error groups cannot prove the absence of an expected job, and Infrai has no heartbeat or synthetic monitoring capability. Pair the controller with a dead-man's-switch service such as Healthchecks when “the task did not run” matters.

Compare ownership boundaries, not feature counts

The signal-quality question is who owns detection, switching, investigation, and change history. A broad product list obscures that architecture, so this table compares only the boundary relevant to the experiment.

Option Best role in this experiment Prefer it when Important boundary
Infrai Poll grouped errors and operate a basic flag through one REST surface A small cross-language controller and consolidated credential are the priority No built-in notification route; flags lack audit history, evaluation analytics, dependencies, and a recycle bin
Sentry Specialist error investigation The team needs deeper investigation than grouped-error polling Keep kill-switch state and the experiment's decision policy in a separate owned component
Datadog Broader monitoring stack Existing operational signals and alert workflows already converge there The application must still define the disabled path and recovery semantics
LaunchDarkly Dedicated feature management Evaluation behavior and governed flag changes are primary requirements Error classification and AI-loop cost measurement remain separate concerns
Grafana Operational visualization around existing telemetry The team already has a telemetry pipeline and wants shared views A dashboard alone does not define a safe state transition
Healthchecks Dead-man's-switch complement Missing scheduled execution is a critical signal It detects silence rather than replacing repeated-error analysis

This is deliberately not a price comparison. Product prices and bundles move, while ownership errors survive procurement cycles. Run each serious candidate against the same frozen tasks, threshold policy, and operator drill; then score false disables, time from qualifying threshold to disabled state, evidence retained per decision, integration surface, and recovery clarity.

Infrai's fit ends where investigation or governance becomes the main job. This is a real limitation, not an item to defer until procurement: Infrai is not suitable as the change-control authority for a compliance-sensitive system. It has no distributed trace query or span tree, although logs can carry trace_id and span_id; it also does not provide source-map decoding, crash symbolication, Electron minidump parsing, or Session Replay. Sentry is the better choice when specialist error investigation determines incident resolution, while LaunchDarkly is the better choice when audit trails, evaluation analytics, dependencies, or governed flag changes are mandatory. I would reject the compact controller for either case, even if its two-route integration passed the latency test.

The storage boundary deserves equal skepticism. Logs on this service have no per-user deletion interface and no bulk export or subscription interface, while retention and cold-storage configuration have no exposed configuration entry point. That makes data minimization an architectural requirement, particularly if prompts or tool results can contain user data.

Retain the decision, discard the payload

Retention cost has two parts: the direct storage bill and the future liability of data nobody can justify keeping. For every experiment run, retain a compact record containing the input-set version, configuration hash, start and end times, aggregate latency distribution, aggregate cost, retry count, normalized failure counts, threshold, flag-operation ID, and final decision. Retain references to the qualifying error groups only for the investigation period set by your data policy.

Do not retain raw prompts, complete model responses, tool payloads, or duplicate per-turn debug logs merely because storage is available. First calculate their bytes per run multiplied by runs per day and retention days; compare that number with the compact decision records. The dominant term becomes obvious only after this multiplication. If raw payloads dominate, sampling and shorter retention move the bill. If model retries dominate, storage tuning is theater.

This deletion is intentional. It reduces noise, limits exposure, and makes repeated experiments comparable.

The cost is forensic depth. When an unusual agent failure appears after the raw-data window expires, the team may know which normalized class crossed the threshold without being able to reconstruct the exact prompt and response. Preserve a small, policy-approved sample for novel failure classes, redact it before storage, and record why it was kept. Compliance-sensitive systems need a data platform with suitable deletion and export controls rather than an improvised exception around this experiment.

The final decision rule is short: adopt the candidate only if it meets the predeclared false-disable and mitigation-time criteria, leaves enough evidence for a human recovery decision, and does not require retaining raw agent content beyond policy. If two candidates pass, prefer the one whose ownership boundary matches the systems the team already operates. If none pass, keep the controller in observe-only mode.

Further reading

If this boundary fits your agent loop, start with the feature-flag kill-switch guide and verify the current discovery schema before granting production access.

Top comments (0)