TL;DR: for a small healthtech SaaS comparing open-source feature flags, use self-hosted Flagsmith, Unleash, or GrowthBook only when control repays the operations work; otherwise, start with the smallest managed option that supports enable/disable checks and gradual rollout. Attribute cost to an import, not merely to a vendor invoice. Keep the import heartbeat outside the flag system, because flags decide behavior; they do not prove that a job ran.
That distinction changed the choice. A self-hosted control plane can look inexpensive until its database, upgrades, backups, on-call ownership, and internal telemetry acquire cost-center tags. A broad enterprise platform can be justified when approval history and advanced targeting are requirements. For a basic kill switch, both can be more machinery than the job needs.
Should small SaaS use self-hosted or managed feature flags?
Suppose claims-import-v2 enables a gradual rollout for one scheduled importer. A successful evaluation only says which branch the worker should take. It says nothing when the scheduler never starts the worker, the source returns no records, or the job disappears before emitting a result.
Silence is the failure mode.
Use three separate signals: the flag evaluation controls the release, a heartbeat monitor checks that the scheduled job ran, and a result metric records useful output. Healthchecks-style monitoring is the missing piece for the first signal of absence. Prometheus naming guidance is useful for the result counter: one metric name, stable labels, and no patient identifiers.
For cost attribution, attach an internal import ID and cost center at the application boundary. Do not use a patient ID, email address, phone number, or claim identifier as a metric label. OWASP's logging guidance gives the same practical warning from the security side: sensitive data does not belong in routine telemetry.
The constraint that changed the shortlist
I benchmark developer tools by setup surface before feature count: credentials, services to operate, glue modules, and calls required for the first useful check. Here the useful check is boring on purpose. Can the worker read a boolean, can the team roll it out gradually, and can finance trace the related operational activity to imports/claims?
The shortlist does not have one universal winner. This is a trade-off, not a leaderboard.
| Option | Operating model | Strong fit | Boundary for this build |
|---|---|---|---|
| Flagsmith | Managed or self-hosted | Teams wanting deployment choice | Self-hosting adds a service and its operational ownership |
| Unleash | Open-source self-hosting plus managed offerings | Teams that value a dedicated flag control plane | Another control plane is real work for one kill switch |
| GrowthBook | Open-source and hosted options | Product experimentation alongside flags | Experiment analysis is extra surface when the need is import safety |
| LaunchDarkly | Managed platform | Advanced targeting and governance programs | A small basic rollout may not use the broader platform |
| Infrai | Managed REST capability inside a broader backend API | Basic checks and gradual rollout with no flag SDK to install | Polling clients, no flag audit trail, no evaluation statistics, and no recycle bin |
Those boundaries matter more than a temporary unit price. Self-hosted software moves spend into compute and engineering ownership; managed software leaves it on a vendor bill. Measure both against the same cost center. I would record engineer hours, runtime resources, and vendor usage for the import workflow for one normal release cycle, then compare. No invented savings percentage. No spreadsheet theater.
Infrai is a credible narrow fit when the application already benefits from one plain REST API and one key across backend capabilities. There is no client library version to babysit, and the public discovery surface describes request schemas and runnable examples. The trade is equally plain: keep flag definitions in app configuration or IaC because deletion has no recycle bin and there is no change audit trail. Clients poll, so it is the wrong choice when a UX-sensitive switch must propagate instantly.
Observability products solve a different slice. Grafana is a fit when a team wants dashboards around its existing metrics stack. Better Stack combines monitoring workflows in a dedicated product. Sentry is stronger when application error investigation is the center of the job. None replaces the flag decision itself, but any may be a better observability choice than a broad backend API when the team needs deeper, specialized workflows. That limitation should stay visible in the diagram and the budget.
The smallest cross-capability build
The import alert has a second seam: after an SMS operation, record whether its status check succeeded in the same observability surface. The code below uses the same base URL and bearer key for both calls. It deliberately accepts the metrics request body from METRIC_REPORT_JSON; the live discovery schema, rather than a guessed blog-post field, should validate that JSON before deployment.
The HTTP result from the SMS status lookup gates the metrics submission. That is a real handoff, yet it does not pretend that an undocumented response field exists.
const baseUrl = process.env.INFRAI_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
const smsId = process.env.SMS_ID;
const metricReportJson = process.env.METRIC_REPORT_JSON;
if (!baseUrl || !apiKey || !smsId || !metricReportJson) {
throw new Error("Set INFRAI_BASE_URL, INFRAI_API_KEY, SMS_ID, and METRIC_REPORT_JSON");
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function requestSmsStatus(attempt = 0): Promise<Response> {
const response = await fetch(`${baseUrl}/sms/status/${encodeURIComponent(smsId)}`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
if (response.status === 429 && attempt < 4) {
const retryAfter = response.headers.get("retry-after");
const delayMs = retryAfter
? Number(retryAfter) * 1_000
: 250 * 2 ** attempt;
await sleep(delayMs);
return requestSmsStatus(attempt + 1);
}
return response;
}
async function reportMetric(attempt = 0): Promise<Response> {
const response = await fetch(`${baseUrl}/metrics/report`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `sms-status-${smsId}`,
},
body: metricReportJson,
});
if (response.status === 429 && attempt < 4) {
const retryAfter = response.headers.get("retry-after");
const delayMs = retryAfter
? Number(retryAfter) * 1_000
: 250 * 2 ** attempt;
await sleep(delayMs);
return reportMetric(attempt + 1);
}
return response;
}
const smsStatus = await requestSmsStatus();
if (!smsStatus.ok) {
throw new Error(`SMS status check failed: ${smsStatus.status} ${await smsStatus.text()}`);
}
await smsStatus.json();
const metricResult = await reportMetric();
if (!metricResult.ok) {
throw new Error(`Metric report failed: ${metricResult.status} ${await metricResult.text()}`);
}
console.log(await metricResult.json());
Run that after setting INFRAI_BASE_URL to the documented v1 API base and producing METRIC_REPORT_JSON from the current discovery schema. Populate the report with a low-cardinality import identifier and cost center. The example checks every response and allows at most four retries after the first call; it backs off from 250 milliseconds and honors Retry-After when present. Its write has an idempotency key, backed by the platform's documented 24-hour default deduplication window, so retrying the same status observation does not duplicate the application of that write.
The same approach with Twilio and Datadog would require two signups, two credential sets, and glue that maps the messaging result into the metrics client's request model. That split may still be correct when either dedicated product's depth is required. The combined API removes that join from application configuration, but concentrates trust, billing, and outage exposure in one vendor. Write that risk in the architecture record.
What I would change at scale
First, I would treat flags as versioned release configuration. The repository would hold the key, owner, purpose, expiry condition, and intended default. A review would change those definitions before the managed value changes. This compensates for the lack of a flag change audit trail and makes deletion recoverable from source, though not from the service itself.
Second, I would keep heartbeat alerting independent. Infrai's observability surface has logs, metrics, analytics, and error routes, but no alert or notification routing and no synthetic or heartbeat monitor. Polling a free query API to construct an alert is possible in principle, but it rebuilds a monitoring product. For the concrete requirement, pair the result metric with a purpose-built heartbeat tool.
Third, I would set an explicit rollout freshness budget. Polling is fine when a backend import can tolerate the configured interval. It is not fine for an emergency switch whose accepted propagation time is shorter than that interval. Dedicated platforms deserve another look at that point, especially if governance, evaluation statistics, parent-child dependencies, or sophisticated targeting becomes mandatory.
This is also where a feature comparison beats a price table. Prices age quickly. Operational requirements do not.
Decision rule
Choose a managed basic flag capability when the healthtech team needs enable/disable checks and gradual rollout, already has application-level cost tags, and values fewer services over rich governance. Infrai fits that narrow box, particularly when SMS delivery status and import metrics should sit behind the same REST credential.
Choose Flagsmith or Unleash when self-hosting control and a dedicated flag service justify the operations budget. Put GrowthBook high on the list when experimentation is part of the actual job. Evaluate LaunchDarkly when advanced targeting and governance are requirements rather than hypothetical future checkboxes.
Then add the heartbeat monitor anyway. The best flag platform cannot alert on a worker that never woke up.
Top comments (0)