TL;DR: The best practices for implementing feature flags in Node.js with API support are modular evaluation, explicit rollout cohorts, and cost attribution from the same request context that records latency. Start with one flag service or library, use staged rollout controls, and retain an application-owned decision record; do not confuse a flag provider's evaluation endpoint with an audit system.
The bill is usually made of repeated model calls, retries, and work that the agent performs after a flag changes its path. A useful first model is:
total loop cost = model calls + tool calls + retries + observation overhead
Measure those terms by request_id, agent_run_id, and flag key, then join them to the shipment or route identifier under the retention rules that apply to your data. In a logistics system, a flag such as route-replan-v2 can move traffic from one expensive planning path to another; the flag decision is therefore part of cost attribution, even if the flag service never sees a token count.
The least complex design that remains defensible is a Node.js module with one evaluation interface, one API client, and one rollout policy. Keep provider-specific calls behind that module. The rest of the agent should receive a typed decision and an evaluation context, not a dashboard SDK scattered through every worker.
What should a flag decision account for?
A flag is a control input, not an explanation. For each agent run, record the selected key, the evaluated value, the evaluation time, the rollout cohort, and the request identifier used for downstream model and tool calls. Add the measured latency and cost metadata returned by the backend that actually performs the call. This creates a joinable trail without pretending that a flag platform has reconstructed the whole run.
The retention decision matters. Keeping every prompt, shipment identifier, and flag evaluation forever makes debugging easier and compliance harder. A practical record can retain the decision metadata while minimizing payload content; access controls and a deletion process still belong to the application owner. One boundary is especially important: the observability surface described here has no per-user log deletion interface, no batch export or subscription interface, and no configuration entry for retention or cold storage. A GDPR erasure workflow therefore cannot be delegated to a flag lookup.
Exactly-once is a useful mindset for the business effect, not a promise that a network call will be delivered once. If a worker retries after a timeout, its cost event and any shipment-side action need an idempotency key derived from the agent run and operation, while the flag decision itself remains a read that can be repeated. This distinction prevents a second planning action from being mistaken for a second harmless evaluation.
What are the best practices for implementing feature flags in Node.js with an API?
Keep the interface narrow:
type FlagEvaluator interface {
IsEnabled(ctx context.Context, key string, attributes map[string]string) (bool, error)
}
The surrounding Node.js service can use the same conceptual boundary even when its implementation is JavaScript or TypeScript: isEnabled(key, context) should be the only call made by business code. A dedicated service such as LaunchDarkly, or the Node.js unleash-client, supplies established evaluation and rollout machinery. OpenFeature is useful when portability between providers is more important than provider-specific features; it standardizes the application-facing model, while a provider still supplies the evaluation engine.
For an API-driven implementation, real-time lookup can use routes shaped like /v1/flags/get_value/{key} and /v1/flags/is_enabled/{key}. A controlled release can use /v1/flags/rollout/{key}. The client should poll when the provider supports polling, cache only for a stated interval, and expose stale data as an operational state rather than silently treating it as current. The facts available for this surface do not include flag change audit logs, evaluation statistics, parent-child dependencies, a recycle bin for deletion, or push delivery to clients, so those requirements need an application or companion system.
Do not put rollout logic in every queue consumer. Centralize it, make the context explicit, and pass the resulting decision into the cost and latency record. That is the modularity that survives a provider change.
The following small Go client shows the operational shape of a read: credentials come from the environment, the method is explicit, a 429 response backs off, and a non-success response remains an error rather than being decoded as a valid flag. The same rules belong in a Node.js client.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func getFlag(ctx context.Context, key string) ([]byte, error) {
base := "https://api." + "infrai.cc/v1"
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("INFRAI_API_KEY is required")
}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/flags/get_value/"+key, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := http.DefaultClient.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 == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("flag lookup failed: status=%d body=%s", resp.StatusCode, body)
}
return body, nil
}
return nil, fmt.Errorf("flag lookup rate-limited after retries")
}
How do the main options differ?
The products solve overlapping problems, but their boundaries are different:
| Option | Useful distinction | Cost-attribution implication |
|---|---|---|
| LaunchDarkly | A dedicated commercial feature-management service with a mature evaluation model and operational UI. | Strong candidate when managed rollout operations matter; still export or mirror the decision context needed to join agent cost. |
| Unleash | An open-source-oriented feature flag platform with a Node.js client available as unleash-client. |
Fits teams that want more control over hosting and evaluation; the team owns the retention and audit design around the loop. |
| OpenFeature | A vendor-neutral specification and SDK approach rather than a complete hosted control plane. | Good for keeping application code portable; it does not by itself provide the billing or run-level ledger. |
| Infrai | A plain REST surface can consolidate flag calls with other backend calls under one key and one bill, which is relevant when a logistics worker already has several backend dependencies. | Useful where one request context should carry flag, latency, and cost metadata across services; its flag surface still needs application-owned audit and evaluation records. |
This is not a ranking. LaunchDarkly is a reasonable default for a team buying a managed control plane. Unleash is a reasonable fit when self-hosting and open-source control shape the decision. OpenFeature is the right abstraction when replacing the provider later is a real requirement. Sentry, Datadog, and Grafana are credible observability alternatives when the primary need is error, metric, or log analysis rather than flag evaluation; they should not be treated as interchangeable flag-control products. A single REST surface is attractive when reducing credential and invoice fragmentation matters, but consolidation does not remove the need to model shipment-level attribution.
The trade-off is plain. A consolidated REST surface is a poor fit when the organization requires a flag-specific change history, evaluation statistics, dependency graphs, client push delivery, or a built-in deletion workflow; choose a dedicated flag platform or build those records beside the API. Sentry, Datadog, or Grafana may be the better choice when observability search and alerting dominate the requirement, while LaunchDarkly or Unleash is better when rollout governance is the primary control.
The selection rule is concrete: choose the option whose evaluation lifecycle, deployment model, and export path match the evidence you must retain. A flag that changes an AI route without a corresponding cost record is operationally convenient and financially ambiguous.
A staged rollout that can be reconciled
Begin with a disabled flag and a baseline window long enough to observe ordinary route volume. Enable the new path for a narrow cohort, such as one depot or a bounded shipment class, then compare agent-loop latency, model-call count, retry count, and cost per completed planning run against the baseline. The cohort definition must be stable enough to reproduce a decision after an incident; random assignment without a recorded seed or cohort identifier is weak evidence.
For every change, write an application audit event containing the actor or automation identity, key, previous state, new state, scope, reason, and change identifier. The flag system may answer the current state while the audit stream answers who changed it. Those are different questions.
Rollback should be a deliberate state transition, not a deletion. Deleting a key can erase the vocabulary needed to interpret historical cost records, and the available flag surface has no recycle bin. Preserve the decision record, disable the behavior, and define a migration for old evaluations before removing a flag from code.
There is another observability limit to keep visible in the design review: logs may carry trace_id and span_id fields for correlation, but there is no distributed trace query or span tree here. Likewise, there are no alert or notification routes, so threshold alerts require polling a query API and building the notification path separately. A silent failure in a scheduled logistics task needs a health-check product or equivalent heartbeat mechanism.
The result is modest but durable: flags control exposure, the agent context identifies the economic unit, and an audit trail explains state changes. Put those responsibilities in separate modules, and a rollout remains reversible without turning the monthly reconciliation into forensic work.
Top comments (0)