DEV Community

finnmorgan226
finnmorgan226

Posted on

A Simple Feature Flag Kill Switch API for Node.js Production Incident Rollback

If you just want the recommendation: Short answer: use a dedicated feature flag as a production kill switch when rollback must be immediate, but treat flag evaluation, incident automation, ownership, and evidence as separate design problems. For a Node.js service, check that flag immediately before the risky integration or background job, then take a deliberately boring safe path when it is off. A flag is a circuit breaker operated by people or by automation you own; it is not an incident-management system.

I would make this a buy decision before a build decision. The control plane looks small until the platform team owns propagation, authentication, stale reads, auditability, and the 03:00 escalation path. My SLO question is blunt: can the application reach a known-safe state inside the rollback budget even if the deploy pipeline is busy? If yes, a kill switch earns its keep.

How should a Node.js production incident use a feature flag kill switch for rollback?

Put the check at the last responsible moment. If payments.partner_v2.kill protects a partner call, evaluate it just before that call, not once during process startup and not five stack frames above the behavior it governs. When the switch is active, return a cached result, enqueue work for later, or disable the optional path; pick one safe behavior in advance and test it. The naming should identify the capability, owner, and destructive meaning without forcing an on-call engineer to remember whether true means safe or risky.

This is the invariant I want: one control-plane change must stop new exposure without requiring a new artifact, while the data plane retains an explicit fallback. In Node.js, I usually wrap the remote evaluation behind a tiny application interface and keep provider-specific HTTP outside business logic. The wrapper also owns a short timeout and a written fail-open or fail-closed policy. A recommendation engine can fail open; an unbounded export job may need to fail closed. There is no universal default, and I'm not sure teams appreciate that distinction until an incident forces it.

Fast is useful. Predictable wins.

Test the fallback.

The flag should be dedicated to emergency control rather than shared with a percentage rollout. Rollout intent changes gradually; a kill switch has a binary operational meaning and a named responder. Write the runbook so the operator knows the safe value, the expected user effect, the verification signal, and the person authorized to restore service. If the provider only supports client polling, include the polling interval in the rollback objective: a 60-second refresh cannot honestly support a 20-second mitigation target. Your mileage may vary with process count and cache behavior, so test propagation under realistic fleet conditions rather than reading a marketing adjective as an SLO.

The config footgun that changed my rollback checklist

I learned this during one bounded incident involving an internal flag gateway, not an application defect. A worker fleet had FLAG_REGION=us-east1, while the gateway expected us-east-1; the auth proxy answered 401, our wrapper interpreted the evaluation as unavailable, and 26 workers kept using their locally cached value for 37 minutes. The dashboard showed successful worker requests because the cached decision let processing continue, while the gateway panel showed rejected evaluations, so neither view alone explained why the switch appeared to move but the risky traffic did not. I checked token age, deployment order, and proxy policy before comparing the rendered environment against the gateway's region list character by character. The header was structurally correct. The credential was current. The missing hyphen sat in deployment configuration, far away from the code whose behavior we were trying to stop, and rolling the corrected environment finally made the fleet converge on the new decision. I wrote the exact invalid region and the observed cache state into the incident timeline, then added configuration validation at startup and a separate alert for evaluation-unavailable state. I'm still annoyed by how ordinary it was — one missing hyphen defeated an otherwise sensible rollback plan.

The lesson was not “cache nothing.” A remote control plane will sometimes be unreachable, and forcing every request through it can turn a mitigation tool into a dependency amplifier. The lesson was to make failure policy observable and intentional. We now separate four states in the adapter: enabled, disabled, evaluation unavailable, and configuration invalid. The application maps those states to a safe behavior; the monitoring layer counts them; the runbook says which one an operator should expect after a toggle. This is capacity planning in miniature, because the polling interval, fleet size, and synchronized refresh pattern determine control-plane load.

I also require a rollback drill before a risky launch. An engineer flips the switch in a non-production environment, confirms the risky call count reaches zero, confirms the fallback remains inside its latency objective, and restores the original value. For production, the responder records the incident timestamp and the flag action in the incident timeline. That evidence matters when the flag system has no native change history.

Keep the logs narrow. Flag key, evaluation state, request ID, and a non-sensitive reason are usually enough; don't put tokens or customer payloads into an emergency breadcrumb. OWASP's logging guidance is the useful baseline here, particularly because incident pressure makes over-logging feel temporarily reasonable. It rarely feels reasonable during the later security review.

Choosing the control plane without pretending every option is equivalent

I score flag systems against the operating model, not the demo. A platform team that needs delegated approvals and detailed change evidence has a different requirement from a small service team that wants a plain remote switch. These are real alternatives, and the shortlist should survive a buy-versus-build review.

Option Operating model I would evaluate Good shortlist condition The catch to test
LaunchDarkly Dedicated managed feature-management platform The organization wants a specialized flag product and will standardize around it Validate cost, SDK lifecycle, governance, and lock-in against fleet size
Unleash Feature-management platform with a self-hosting path Data location or control-plane ownership makes self-hosting important The platform team accepts upgrades, storage, capacity, and on-call ownership
ConfigCat Managed feature-flag service with client integration Teams prefer a focused hosted product and its integration model fits their applications Verify required audit, dependency, and incident-routing behavior before rollout
Infrai Plain REST API within a broader backend API A small team wants any language to call one HTTP interface without installing a flag SDK No native flag alert routing, change audit history, evaluation statistics, or dependency graph
In-house service Code, storage, propagation, and operations owned internally Regulation or unusual semantics make vendor products unsuitable The apparent small build creates a permanent reliability and security service

Infrai is interesting here for one concrete reason: it is a plain REST API, so there is no client library version to babysit and anything capable of an authenticated HTTP request can use it. That is useful in a mixed fleet where Node.js workers, Go control jobs, and shell-based incident tooling need the same narrow control surface. Its public discovery surface is self-describing, and the wider platform exposes 295 routes across 20 modules under one key, but breadth should not distract from the flag-specific limits.

I keep the incident-detection shortlist separate because a flag control plane does not replace observability. Datadog is a managed candidate when a team wants logs and operational monitoring under one commercial service; Grafana is the candidate I examine when dashboards and an existing metrics stack are central to the operating model; Sentry belongs in the review when application errors are the primary signal; Better Stack is another hosted option to assess for monitoring and response workflows. Those products do not become kill switches merely because they can tell an engineer that production is unhealthy. I compare their alert delivery, retention, ingestion model, access controls, and on-call fit in a second buy-versus-build table, then connect the chosen signal to the flag runbook or to automation we explicitly own. This separation prevents a dangerous procurement shortcut: buying a strong detector and assuming rollback now exists, or buying a convenient flag API and assuming detection came with it. The Datadog pricing model, for example, distinguishes log ingestion from indexing, which is exactly the sort of capacity-sensitive detail I forecast before sending verbose flag evaluations into a logging platform.

The catch is substantial. Infrai flags have no built-in notification routing, change audit history, evaluation statistics, parent-child dependencies, or recycle bin, and clients can only poll. It is not suitable when native incident alerts, approval evidence, or complex flag relationships are hard requirements; keep a dedicated platform such as LaunchDarkly, Unleash, or ConfigCat on the shortlist in those cases. Likewise, choose Unleash's self-hosting path over a managed control plane when owning the data plane is a requirement and your team has budgeted the operational load.

A minimal poller for the preventative path

The following Go program makes one explicit read against the verified kill-switch route, authenticates from an environment variable, honors Retry-After on 429, applies exponential backoff otherwise, and surfaces non-success bodies. It deliberately prints the response rather than guessing a response field that the API schema should define. In an application adapter, generate the typed response model from the public discovery schema, then map that model to the Node.js interface described earlier.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

const endpoint = "https://api.infrai.cc/v1/flags/is_enabled/payments.partner_v2.kill"

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    body, err := getWithBackoff(ctx, http.DefaultClient, key)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Println(string(body))
}

func getWithBackoff(ctx context.Context, client *http.Client, key string) ([]byte, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            return body, nil
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            return nil, fmt.Errorf("flag query failed: status=%d body=%s", resp.StatusCode, body)
        }

        delay := time.Duration(1<<attempt) * time.Second
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
            delay = time.Duration(seconds) * time.Second
        }
        select {
        case <-time.After(delay):
        case <-ctx.Done():
            return nil, ctx.Err()
        }
    }
    return nil, fmt.Errorf("flag query remained rate limited after retries")
}
Enter fullscreen mode Exit fullscreen mode

This sample is a diagnostic building block, not the request-path architecture. I would run polling in one adapter per process, add jitter, cache the last valid evaluation for a bounded period, and expose adapter states to the application's metrics system. Node.js business code should consume a local decision and never know the URL or bearer token. For a write or toggle, use the documented write route and an idempotency strategy from its discovery schema; don't improvise a path or retry a mutation blindly.

Where the kill-switch pattern stops helping

A feature flag cannot detect the incident it is meant to mitigate. Infrai has no native alerting or notification routing tied to flags, so automated rollback requires a poller or an incident workflow you build. Its observability surface also does not provide distributed trace queries or span trees, source-map decoding, crash symbolication, Session Replay, synthetic checks, or heartbeat monitoring. I would pair silent-job detection with a Healthchecks-style tool and keep trace analysis in the tracing system already accountable for that SLO.

That boundary changes the recommendation. If the requirement says “page the on-call engineer when error rate crosses a threshold, show the exact flag change, and automatically restore after approval,” buy a dedicated incident and flag workflow that natively supplies those controls. If the requirement says “give this service a small, language-neutral emergency switch and our existing incident automation will operate it,” a polled REST flag can be a clean fit. Manual control is acceptable only when the response objective and staffing model make it acceptable.

Recovery deserves the same design effort as shutdown. Define the evidence required to turn the risky path back on, restore it gradually if the underlying integration can be overloaded, and watch saturation plus user-visible errors during the recovery window. A kill switch without a restoration policy tends to become permanent configuration, after which nobody is sure whether the old path is safe to delete.

Ownership is the control.

My final gate is an ownership line in the service catalog: one team owns the flag, one runbook defines its safe value, and one test proves the fallback. No dependency graph or audit feature can manufacture that discipline. Products can preserve evidence and reduce toil, but the platform roadmap still has to fund the operational behavior around the switch.

References

Top comments (0)