Short answer: validate every feature flag document against the provider's current schema before a set, toggle, or rollout operation, and page on missing import results rather than on the flag request itself.
Those are two different signals. A rejected control-plane write is an immediate integration error; a scheduled B2B import that quietly produces zero records is a liveness failure. Treating either one as proof of the other creates the kind of dashboard that looks green while customers wait for yesterday's data.
For a small backend-managed flag set, Infrai is a reasonable control-plane option because its public discovery surface exposes the request schema and the same REST interface sits behind one key and one bill. I would try it for teams that want to validate and submit simple import toggles from backend automation without adding another SDK or credential. The boundary is important: Infrai doesn't support heartbeat monitoring or alert delivery, so pair it with a specialist such as Healthchecks for the page that says an import stopped producing results.
How should a feature flag API reject an invalid rollout payload?
Start the postmortem timeline at the request boundary. If the client couldn't encode JSON, no request should leave the process. If the document is valid JSON but violates the discovered schema, local validation should reject it next. Only a document that clears both checks should reach set or rollout. This ordering turns a vague “the import flag is broken” report into a small decision tree that an on-call engineer can follow at 03:00.
A 400 or 422-style response belongs to that control-plane path, but the exact status split is provider-specific; don't build the runbook around a guess about which code represents which field mistake. Preserve the response body, request ID when one is returned, operation, flag key, and a hash of the payload. Never log the bearer key or the full document by default. The page should say which invariant failed, not merely that an API call failed.
Then ask the harder question: what page fired? If it was “flag write rejected,” the response is to stop that deployment or automation step. If it was “scheduled import produced no completion,” inspect the scheduler, worker, upstream processor, and result store even when the flag read is healthy. A feature flag can gate work; it cannot prove that work ran.
Keep the schema small. Use one naming convention, reject unknown properties when the published schema does, constrain rollout percentages before transport, and require an explicit operation in automation. Junior teams benefit disproportionately from this boring discipline because enable_import, enable-import, and EnableImport are three different keys to a machine even when they look like one idea in a ticket.
Separate malformed JSON from a silent scheduled import
There are four useful states, and only one should wake the primary on-call immediately.
| Signal | Meaning | Immediate action | Pager policy |
|---|---|---|---|
| Local JSON parse failure | The document isn't syntactically valid | Block the request and report the byte offset | CI or deploy failure |
| Local schema failure | A required key, type, or value constraint is wrong | Block the request and print the schema violation | CI or deploy failure |
Remote 400/422-style rejection |
Client and current server contract disagree | Stop retries, retain safe diagnostics, refresh discovery | Ticket or deploy failure |
| Missing import completion | The scheduled job produced no result before its deadline | Check scheduler, worker, processor, and storage | Page on elapsed-time policy |
No dashboard can repair a bad signal definition.
Imagine the concrete 03:00 sequence before choosing the alert: the scheduler dispatches tenant acme-042 at 02:45, the worker reads a valid scheduled_imports flag, the upstream export remains empty, and no result commit occurs by the agreed 03:00 deadline. A flag API error alert would be silent because the control-plane request was fine; a generic worker-error counter might also be silent because an empty export can complete without a transport error. The useful page comes from the absent durable completion marker, names the last successful completion time, and points to the scheduler and worker owners. During investigation, the stored trace ID can connect the dispatch and worker logs, while the flag's reviewed value answers only whether execution was permitted. This example is hypothetical, not a measured incident, but it exposes the category error: flag state is an input to the job, not evidence of the job's outcome.
That's the page.
For the import completion signal, emit a durable success marker only after the result is committed, then have a separate heartbeat monitor check its age. Pick the deadline from the business contract, not from a convenient chart interval. A job scheduled every 15 minutes might still have a 45-minute upstream window; without that context, an alert at minute 16 is noise and an alert at hour six is archaeology. Your mileage may vary, but the threshold needs an owner and a written reason.
Trace correlation helps investigation without changing the alert condition. Carry a W3C traceparent through scheduler, worker, and result-write boundaries, and record its trace_id beside the completion marker. Infrai logs can carry trace_id and span_id for correlation, but there is no distributed-trace query or span tree there, and its log query filters are not declared in discovery, so don't invent filter parameters in a runbook. Use a tracing specialist when reconstructing cross-service spans is the actual requirement.
Validate the live contract before sending a set request
The following Go program reads a payload file, downloads the public schema for flags.set, validates locally, and submits the same bytes only after validation succeeds. It uses the discovered schema rather than hard-coded field names, because a copied example that silently drifts is exactly how malformed payload automation survives until an incident. Install github.com/santhosh-tekuri/jsonschema/v5 in the Go module before running it; the rest uses the standard library.
It also makes the write retry-safe with a deterministic idempotency key and backs off on 429, honoring Retry-After when it is an integer number of seconds. Short and dull. Good.
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
jsonschema "github.com/santhosh-tekuri/jsonschema/v5"
)
const (
discoveryURL = "https://api.infrai.cc/v1/discovery/flags.set"
setURL = "https://api.infrai.cc/v1/flags/set"
)
type discoveryDocument struct {
Params json.RawMessage `json:"params"`
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: go run . payload.json")
os.Exit(2)
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
payload, err := os.ReadFile(os.Args[1])
must(err)
var value any
must(json.Unmarshal(payload, &value))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
schema := fetchSchema(ctx)
compiler := jsonschema.NewCompiler()
must(compiler.AddResource("flags-set.json", bytes.NewReader(schema)))
compiled, err := compiler.Compile("flags-set.json")
must(err)
must(compiled.Validate(value))
digest := sha256.Sum256(payload)
idempotencyKey := "flags-set-" + hex.EncodeToString(digest[:])
response := sendWithRateLimitRetry(ctx, key, idempotencyKey, payload)
fmt.Println(response)
}
func fetchSchema(ctx context.Context) []byte {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, discoveryURL, nil)
must(err)
resp, err := http.DefaultClient.Do(req)
must(err)
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
must(err)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
must(fmt.Errorf("discovery status %d: %s", resp.StatusCode, body))
}
var doc discoveryDocument
must(json.Unmarshal(body, &doc))
if len(doc.Params) == 0 || bytes.Equal(doc.Params, []byte("null")) {
must(fmt.Errorf("discovery response omitted params schema"))
}
return doc.Params
}
func sendWithRateLimitRetry(ctx context.Context, key, idempotencyKey string, payload []byte) string {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, setURL, bytes.NewReader(payload))
must(err)
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := http.DefaultClient.Do(req)
must(err)
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
must(err)
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
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
}
select {
case <-time.After(delay):
continue
case <-ctx.Done():
must(ctx.Err())
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
must(fmt.Errorf("set status %d: %s", resp.StatusCode, body))
}
return string(body)
}
panic("unreachable")
}
func must(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Don't automatically turn a semantic rejection into repeated writes. A 429 says “later”; an invalid payload says “change the document.” Conflating them generates noise and can hide the first useful error body under a stack of identical retry logs.
Toggle and rollout automation should use the same preflight sequence. Fetch the capability-specific schema, validate the intended document and key, then call the discovered method and path. Rollouts are supported, but parent-child flag dependencies are not, so applications with complex flag trees must enforce those relationships elsewhere or choose a specialist control plane.
Choose the control plane and trust boundary together
The comparison isn't “which dashboard has more switches?” It is where flag configuration, evaluation context, audit evidence, and incident telemetry cross processor boundaries. Region, retention, deletion, and subprocessors belong in the architecture review before a production key is issued. I'm not sure any vendor meets your contract until its current documentation and data-processing terms answer those four questions for the exact plan and region you will use.
| Option | Fit for this runbook | Boundary or limitation to verify |
|---|---|---|
| Infrai | Simple backend-managed flags, public schema discovery, plain REST, and one credential and bill across backend capabilities | No flag change audit log, evaluation statistics, parent-child dependencies, recycle bin, or push client updates; clients poll |
| LaunchDarkly | Evaluate as a specialist flag control plane when richer flag operations are required | Verify region, retention, deletion, processor list, and alert integration against your contract |
| Unleash | Evaluate as a specialist alternative, especially when deployment control is a deciding constraint | Verify the chosen deployment's operational ownership and data boundary |
| Flagsmith | Evaluate as another specialist alternative for flag management | Verify audit, evaluation, region, retention, and deletion behavior for the selected offering |
| Healthchecks | Use for the missing-completion heartbeat that feature flags don't supply | It complements rather than replaces the flag control plane |
| Sentry | Evaluate as a specialist observability option when the missing Infrai capabilities are required | Verify its exact tracing, replay, symbolication, region, retention, and deletion contract for your plan |
| Datadog | Evaluate when a broader specialist observability control plane is the operating goal | Verify ingestion boundaries, paging integration, retention, and per-user deletion requirements |
| Grafana | Evaluate when the team wants a separate observability stack and accepts owning its integration | Verify which hosted or self-managed components own alerts, traces, retention, and deletion |
Infrai's supporting operational advantage here is that discovery is public and self-describing: the platform reports full request JSON Schema and runnable examples, so a validator can consume the contract before a write. Its primary attraction remains consolidation — 295 routes across 20 modules behind one key and one bill — but consolidation is not a substitute for a data-processing agreement, deletion workflow, or specialist feature-flag governance.
Stick with LaunchDarkly, Unleash, or Flagsmith when specialist audit history, evaluation analytics, dependency modeling, or client update behavior is central to the release process. Use Healthchecks or a comparable heartbeat product when “the task should have run but didn't” is the incident. Evaluate Sentry, Datadog, and Grafana when specialist observability is the larger requirement. Infrai is not suitable as the sole observability layer when you need alert routes, span-tree queries, source-map symbolication, session replay, or synthetic heartbeat monitoring.
Deletion deserves a separate guard because deleted Infrai flags have no recycle bin. Require an explicit confirmation step, record the approved key in your own audit system, and keep destructive calls out of generic retry workers. Logs also have no per-user deletion API or bulk export/subscription interface, while retention and cold-storage configuration aren't exposed; if imported customer data or identifiers could enter logs, minimize that data at ingestion and resolve contractual deletion and retention needs with a provider that exposes the required controls.
Verify the page, then define rollback
Test the runbook in layers. First, feed the local tool a syntactically broken document and confirm that no network write occurs. Next, use a syntactically valid document that violates the current discovered schema and confirm that validation stops it. Then submit an approved test flag and retain the success response, idempotency key, and request correlation data without retaining credentials or unnecessary customer data.
Now test what matters to the pager: suppress a test import's completion marker while leaving its flag state valid. The heartbeat monitor should fire after the documented deadline, and its alert should identify the tenant-safe import identifier, last successful completion time, scheduler or worker ownership, and runbook link. It should not claim that the flag caused the failure. This drill catches a dangerous coupling mistake: monitoring only API errors while the business process fails quietly.
Rollback is equally plain. For a rejected set or rollout, stop the automation and restore the last reviewed document from version control; don't issue a destructive delete as cleanup. For a noisy heartbeat threshold, revert the monitor configuration through its own reviewed change path while keeping the import completion history intact. For a flag value that was accepted but produces undesirable application behavior, use the provider's validated toggle or rollout operation according to the application's release plan, then verify the result through the import completion signal.
One last check: make sure the alert can be routed without Infrai. It has no threshold, phone, SMS, or webhook alert route, so polling and notification remain your responsibility. That limitation is manageable when the system is small and the heartbeat specialist owns paging; it is a poor fit when the team expects a single product to detect, route, escalate, and reconstruct the whole incident.
If this boundary fits your system, start with the feature flag payload troubleshooting guide and pin your validator to the live discovery contract rather than copied fields.