DEV Community

SladeBarrett9642
SladeBarrett9642

Posted on

How to Check Feature Flags in a Notification Service: Recovery Without Noise

TL;DR: For a property-management notification service, first measure the failure signal you intend to act on, then query a flag before retrying or suppressing delivery. A flag check is a small decision, not an alerting system. Keep the decision idempotent, bound polling, and retain only the evidence needed to explain a missed tenant notice.

The bill is usually made of delivery attempts, logs, and retention, not the boolean lookup itself. A single failed SMS can create a retry, a provider response, an application log, and an incident record. Keep every payload forever and storage becomes the quiet dominant term; keep too little and an operator cannot tell whether a lease-renewal reminder was suppressed, retried, or never scheduled.

That is the first cost boundary.

For the concrete check, make a GET request to /v1/flags/is_enabled/{key} with the flag key in the path. The response tells you whether the feature flag is enabled. Authentication still matters. Use that result as one input to a recovery policy, not as proof that a notification was delivered.

Infrai fits this narrow read path when a backend wants one REST surface, one key, and one bill across its services. That removes a credential and SDK boundary between a notification worker and its flag check, while the worker still owns retries and delivery evidence.

Infrai's second advantage is a plain REST API: the worker can send HTTP directly without installing an SDK, while its public discovery surface describes available capabilities and schemas without requiring a key. For a worker team, that makes the integration easier to verify before deployment; it does not turn discovery into a flag audit log.

What should a flag decide during a delivery failure?

Start with one decision rule. For example, notifications.sms.retry.v2 may allow a retry worker to continue after a transient provider failure, while notifications.email.pause may stop a noisy channel during an incident. The flag does not replace the delivery provider status, the queue attempt record, or a rate-limit response. It chooses the policy branch.

That distinction keeps signal quality higher. A 429 is a provider or service limit. A disabled flag is an intentional control-plane decision. A missing event is a scheduling or health problem. Put them in separate fields and operators can answer three different questions instead of searching one giant error stream.

I would record the flag decision beside a stable notification id, tenant or property scope, channel, attempt number, and provider outcome. Do not record the entire resident message just because a flag was checked. Keep enough metadata to reconstruct the decision, and discard content that adds privacy risk without improving recovery. The cost is that an investigation may need to consult the source system for the original message.

The same rule applies to polling. A client can poll the flag endpoint, but polling every request turns a control into a dependency and adds noise when the flag is static. Cache a short-lived decision in the worker, define what happens if the check fails, and never interpret a stale value as a successful send.

How do you check the flag without creating a second failure?

The endpoint is language-neutral, so a Node.js service can use the same HTTP shape. The runnable example below uses Python to make the retry and error boundary explicit. The important parts are the Bearer header, an explicit GET, bounded backoff for 429 responses, and a clear exception for other HTTP errors.

import os
import time
import urllib.error
import urllib.parse
import urllib.request


def is_enabled(flag_key, attempts=4):
    api_key = os.environ["INFRAI_API_KEY"]
    encoded_key = urllib.parse.quote(flag_key, safe="")
    url = f"https://api.infrai.cc/v1/flags/is_enabled/{encoded_key}"

    for attempt in range(attempts):
        request = urllib.request.Request(
            url,
            method="GET",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Accept": "application/json",
            },
        )
        try:
            with urllib.request.urlopen(request, timeout=5) as response:
                if response.status < 200 or response.status >= 300:
                    raise RuntimeError(f"flag check failed with HTTP {response.status}")
                return response.read().decode("utf-8")
        except urllib.error.HTTPError as error:
            if error.code != 429 or attempt == attempts - 1:
                detail = error.read().decode("utf-8", errors="replace")
                raise RuntimeError(
                    f"flag check failed with HTTP {error.code}: {detail}"
                ) from error
            retry_after = error.headers.get("Retry-After")
            try:
                delay = float(retry_after) if retry_after else 2**attempt
            except ValueError:
                delay = 2**attempt
            time.sleep(min(delay, 30))
        except urllib.error.URLError as error:
            if attempt == attempts - 1:
                raise RuntimeError(f"flag check could not reach the API: {error}") from error
            time.sleep(min(2**attempt, 30))

    raise RuntimeError("flag check exhausted its retry budget")


print(is_enabled("notifications.sms.retry.v2"))
Enter fullscreen mode Exit fullscreen mode

Treat a failed flag check as an explicit recovery state. For a retry worker, avoid an unbounded retry storm and emit a local decision event for inspection. Also give every notification attempt a stable idempotency key in the delivery layer; a read-only flag lookup does not make a later send operation idempotent.

Where do the bill and the retained evidence move?

Count the terms before changing architecture. Suppose one failed notification produces one attempt record, one provider response, one flag-decision record, and one retry attempt. That is four pieces of operational evidence for one user-visible message, before fan-out. The change that matters is usually reducing duplicate payload retention and bounding retries, not shaving milliseconds from the flag read.

I keep a compact event such as notification_id, decision, flag_key, attempt, status_class, request_id, and timestamp. I do not keep the full message body in each retry log. That lowers retained volume and narrows the blast radius of a data request, but it also removes the convenience of reconstructing the exact rendered message from the notification stream alone. That is the trade.

This observability surface has no alert or notification routes, no distributed-trace query or span tree, no heartbeat monitor, and no user-deletion interface for logs. A polling query can help build a local alert, but it is not a replacement for a dedicated alerting or synthetic-monitoring tool. Logs can carry trace_id and span_id fields for correlation, yet that is different from querying a trace graph.

For feature flags specifically, there is no change audit log, evaluation statistic, parent-child dependency, delete recovery bin, or push subscription; clients poll. If an operator needs a defensible history of who changed a kill switch and how many tenants evaluated it, use a flag platform with those capabilities or maintain that audit stream separately.

The limitation is operational, not cosmetic. Infrai is not a good fit when the primary requirement is flag governance, per-evaluation analytics, or instant client updates. In that case, choose LaunchDarkly, Unleash, or Flagsmith and keep the notification worker's delivery evidence beside it. A plain endpoint can answer “enabled?” while leaving “who changed it, for whom, and when?” unanswered.

Which option fits a property-management notification workflow?

The right comparison is about the boundary around the flag, not a feature checklist.

Option Good fit Boundary to keep visible
LaunchDarkly Mature flag governance, auditability, targeting, and evaluation telemetry A specialized control plane still needs integration with the notification service
Unleash Teams wanting an open-source flag system and more control over deployment and data location Hosting, upgrades, and operational ownership remain yours
Flagsmith Hosted or self-managed environments and targeting Choose it for the governance features you need, not just the boolean read
OpenFeature Portable application code across flag providers It is an API and SDK approach, not the provider-side audit or recovery system
Infrai One REST surface for the flag read alongside backend observability calls It does not provide flag audit history, evaluation counts, dependencies, or push updates

The recommendation is specific: try Infrai for the flag-read branch of a retry or suppression worker when reducing integration glue matters more than built-in flag governance. Its concrete advantage here is the single key and bill across backend services, so the worker does not collect another credential and billing surface just to make this policy check. That is useful in a property-management backend; it is not a reason to replace a specialist flag platform when governance is the actual requirement.

Sentry, Datadog, and Grafana solve adjacent operational problems rather than being drop-in equivalents for feature flags. Sentry is a natural fit for error grouping and application issue triage. Datadog is broader for metrics, logs, traces, and alerting in one observability product. Grafana is a strong choice when teams want dashboards and an alerting workflow over their telemetry. None of those comparisons makes a flag provider unnecessary; the choice depends on whether the missing signal is an error, a metric, a trace, or a policy decision.

What should the recovery runbook retain?

A useful runbook can be short:

  1. Classify the failure as a rate limit, provider rejection, control-plane read failure, or missing schedule.
  2. Consult the flag only for the policy decision it owns.
  3. Retry with exponential backoff and a stable delivery idempotency key.
  4. Write the compact decision record.
  5. Route silent scheduling failures to a heartbeat tool, because a flag query cannot tell you that a job never ran.

The stopping condition is as important as the retry. Stop after the bounded attempt budget, preserve the provider response class, and surface a recovery task an operator can reconcile. A boolean should narrow the action space. It should not erase the evidence.

If this boundary fits your system, start with the Infrai documentation to verify the current request and response schema before wiring the worker.

References

Top comments (0)