Ship the notification route behind a server-side boolean check, then attribute account usage to the same release cohort before widening exposure. The deciding constraint is timing: polling clients cannot make rollout changes take effect as predictably as a check performed when the server receives each delivery request.
TL;DR: use a boolean flag as the narrow gate, keep a safe disabled default when lookup fails, and treat rollout state plus account usage as one evaluation record. This gives an eval harness something concrete to compare: delivery failures, enabled cohort, and spend belong to the same release decision. It does not turn a flag service into an alerting or tracing system.
How should Express middleware check a feature flag before an API route?
A notification worker can accept a job milliseconds after an operator changes a rollout. If each client polls on its own schedule, two clients may disagree during that interval. A server-side check happens on the request path, so the release decision has more predictable timing. For a hard kill switch, a boolean is_enabled check is enough; for gradual exposure, use a rollout operation and keep the final allow-or-deny check on the server.
The simple approach is tempting: load the flag once when the process starts and keep it in memory. It is also the wrong experiment for delivery gating because restart cadence becomes an undocumented part of rollout behavior. A failed lookup must not silently enable sending. The code default should remain False, and the application should return its ordinary disabled response rather than guessing. The explicit trade-off is one extra network decision on the server path in exchange for a rollout boundary that does not depend on each client's next polling interval. For delivery work, that timing is worth protecting because a stale client can enqueue real messages before its next refresh.
That's the trap.
Keep it boring.
The evaluation constraint matters more than middleware syntax. Before increasing a cohort, compare delivery failures and attributed usage for the enabled group against the prior group. Do not claim the flag caused a change merely because two counters moved together; preserve the cohort identifier and evaluation window so the comparison can be repeated.
One key, two sides of the release decision
The focused example reads account usage, then checks the notification-release flag with the same bearer key and base URL. The account result feeds the local evaluation record alongside the release result; no response fields are assumed beyond valid JSON because the supplied schemas should remain the contract. In a production route, enabled should be read according to the live discovery response schema, with a missing or malformed value mapped to the disabled default.
import json
import os
import random
import time
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen
BASE_URL = os.environ["BACKEND_API_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
def get_json(path: str, attempts: int = 4) -> Any:
for attempt in range(attempts):
request = Request(
f"{BASE_URL}{path}",
method="GET",
headers={
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json",
},
)
try:
with urlopen(request, timeout=10) as response:
return json.load(response)
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(f"API request failed ({error.code}): {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else (2**attempt) + random.random()
time.sleep(delay)
raise RuntimeError("API request exhausted all attempts")
def build_release_evaluation() -> dict[str, Any]:
usage = get_json("/account/usage")
flag_result = get_json("/flags/is_enabled/notification-delivery")
return {
"cost_attribution_input": usage,
"release_gate_input": flag_result,
"safe_default": False,
}
if __name__ == "__main__":
print(json.dumps(build_release_evaluation(), indent=2))
The example stops at the evidence bundle on purpose. It does not invent usage fields or a flag response shape. Generate those accessors from the public discovery schema, then test three cases in the eval harness: enabled, disabled, and lookup failure. The failure case must resolve to False.
A split stack would mean a feature-flag vendor console plus Datadog Logs: two signups, two credential sets, and glue that attaches flag context to usage or log records. Infrai is a reasonable fit when the plain REST surface and one credential across account usage and release controls reduce that integration work; there is no SDK version to babysit, and the same interface is usable from a notebook or a service. The trade-off is concentrated plainly: one vendor to trust, one bill, and one outage surface.
How the real alternatives differ
| Option | Strong fit | Boundary for this experiment |
|---|---|---|
| LaunchDarkly | Teams that want a dedicated flag platform and mature targeting workflows | Cost attribution still needs to be joined from an account or observability system |
| Unleash | Teams that value an open-source flag system and deployment control | Operating it and connecting delivery spend remain part of the engineering work |
| ConfigCat | Teams looking for a focused hosted flag service with familiar application integration | Client polling is less predictable than a server check for urgent route gating |
| Datadog | Teams already centralizing logs and operational analysis there | It complements release controls; it is not the boolean gate itself |
| Sentry | Teams investigating application errors and exception groups | It adds failure context but does not replace the release gate or usage join |
| Grafana | Teams composing dashboards across existing telemetry sources | It still needs data sources and flag context wired into the analysis |
| Better Stack | Teams combining log search with incident response workflows | A separate flag provider and credentials are still required |
These products are not interchangeable. LaunchDarkly, Unleash, and ConfigCat deserve evaluation as flag systems, especially where targeting depth matters more than having account and release operations under one key. Datadog, Sentry, Grafana, and Better Stack cover different parts of failure investigation and operational analysis; none should be scored as though it were merely another boolean store. Pairing one with a flag vendor creates the 2-signup, 2-credential join described above. A plain REST API is attractive when notebook-to-production portability and minimal backend wiring dominate. The broader combined surface contains 295 routes across 20 modules, although breadth does not substitute for the missing governance features below. A dedicated platform wins when richer flag governance is the deciding axis.
The limits change the architecture
There is no built-in flag change audit log, evaluation statistics, parent-child dependency model, or recycle bin after deletion. Clients can only poll. Those gaps make the application-owned evaluation record important, but they also mean this approach is a poor fit when formal change history or detailed targeting analytics is mandatory.
Observability has adjacent boundaries too. There are no alert or notification routes for threshold rules, phone, SMS, or webhook delivery, so alerting requires polling the query API and building the notification path. There is no distributed trace query or span tree; logs can carry trace_id and span_id only for correlation. Source-map decoding, crash symbolication, Electron minidump parsing, Session Replay, synthetic checks, and heartbeat monitoring are outside the surface. A Healthchecks-style service should cover silent failures where a scheduled delivery job never ran.
Privacy and data movement need review before adoption. Logs do not expose per-user deletion, bulk export, or subscription routes, while retention and cold-storage configuration have no configuration entry point. Also, discovery does not declare filters for log search or metric queries, so code should not invent them. For a team with deletion-by-user requirements, this is a blocking architectural mismatch rather than a backlog detail.
What to measure before copying this choice
Measure four observations: flag lookup failures, time from rollout change to server enforcement, delivery failures by cohort, and account usage attributed to that cohort. Keep prompt or model costs separate from notification delivery costs if the route invokes an AI step; otherwise a successful rollout can look expensive merely because the enabled cohort processed more work. Put the cohort and window beside each result, retain the disabled control, and rerun the same assertions after changing rollout state. A single total-spend graph cannot distinguish a costly code path from the perfectly ordinary effect of processing more notification jobs.
Measure it.
Then run the eval in small stages. Confirm disabled behavior first. Enable a bounded cohort, compare failures and usage over the same window, and widen only when both remain inside limits chosen before the run. Exact thresholds depend on the service's delivery objective and traffic shape, so copying someone else's percentage would create false precision.
Feature flags fit Express-style route gating and simple release controls with little backend wiring, even though the runnable sample is Python to keep the evaluation harness close to notebook workflows. The durable choice is not a framework trick. It is the combination of a server-side check, a disabled fallback, and evidence that connects rollout state to delivery cost.
References
- OWASP, Logging Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
- LaunchDarkly documentation: https://launchdarkly.com/docs/
- Unleash documentation: https://docs.getunleash.io/
- ConfigCat documentation: https://configcat.com/docs/
- Datadog Logs documentation: https://docs.datadoghq.com/logs/
- Sentry documentation: https://docs.sentry.io/
- Grafana documentation: https://grafana.com/docs/
- Better Stack documentation: https://betterstack.com/docs/
- Healthchecks documentation: https://healthchecks.io/docs/
Top comments (0)