Short answer: when a deleted and recreated feature flag starts producing 404 Not Found or missing-key evaluations, keep the property import on a known, typed fallback, refresh the evaluator's state, and page only if scheduled imports stop producing fresh results; rollback the release independently of the flag mutation.
That separation is the important trade-off. A hard failure makes configuration damage loud but can stop every property import. A silent default keeps imports moving but can hide a bad control-plane change. The safer design uses a conservative default in the request path and a separate freshness watchdog that can tell the difference between “the toggle is absent” and “the rent-roll import produced nothing.” I've been woken by alerts that meant nothing and missed the one that mattered, so my first question is always: what page fired?
The incident lesson: observe output, not dashboard color
Consider a bounded incident at a property manager. A scheduled import normally writes a completion record containing a building identifier, source cursor, row count, configuration revision, and completion time. An operator deletes a feature flag, later recreates the same human-readable key, and a Node.js worker still holds an earlier snapshot or identity. The next lookup may be reported as a missing key, while a management API lookup may be an HTTP 404. Those two observations are related evidence, not proof that imports have stopped.
The page should fire when the import's promised result is absent beyond its service-level window. It should not fire merely because a dashboard says the flag is red, because the flag store returned “not found,” or because the fallback branch ran once. Dashboards summarize. They don't establish that a lease ledger is current.
This is the invariant: configuration lookup must never be the only evidence of job health. Record both the evaluation and the business result, then correlate them with a run ID. For an illustrative 02:00 run, the useful record is not “flag=false”; it is “run imp-20260816-0200, property p-184, evaluated default, completed at 02:07, 12,481 rows, cursor advanced.” If the completion record never arrives, the watchdog has something concrete to page on. If it does arrive, the missing-key signal belongs in a ticket or deployment warning, not a wake-up call.
How should Node.js services troubleshoot a feature flag 404 after delete and recreate?
Start at the boundary that actually emitted the error. In Node.js, an evaluation library often returns the caller-supplied default plus structured error metadata rather than exposing a transport status directly; an HTTP 404 may instead come from a management endpoint, relay, bootstrap fetch, or custom wrapper. Preserve the original operation name, status, response body category, flag key, environment, and configuration revision. Do not collapse all of them into flag lookup failed.
Then walk the lifecycle in order:
- Confirm the exact environment and project or namespace. A production worker reading a staging key looks identical to a deleted key if the log omits scope.
- Compare the configured key byte for byte, including case and surrounding whitespace. Log a hash if the key itself is sensitive.
- Check the audit trail for deletion and recreation. The same display key does not necessarily prove the recreated object has the same underlying identity; the control plane's audit data and documentation resolve that question.
- Verify that the worker received a configuration revision newer than the mutation. Restarting first destroys useful evidence and turns troubleshooting into ritual.
- Record the evaluation reason and whether the value came from targeting, cached state, or the application default. In OpenFeature's evaluation model, the provider may return an error code and the API returns the default value, which makes the default an explicit part of the contract rather than an accidental rescue.
- Finally, query the import ledger. If cursors continue to advance and completion records remain fresh, configuration is degraded but the job is producing results. If freshness breaches its window, page the import owner and use the last known safe release or operational mode.
Don't retry a genuine missing key in a tight loop. 404 Not Found is defined by HTTP semantics as the origin server not finding a current representation, and it may or may not disclose whether that absence is temporary. Retry only when a documented cache or propagation mechanism gives you a bounded reason to expect change; use backoff and a deadline. I'm not sure what any particular flag service preserves across recreation without its audit record, and guessing from the reused key is exactly how stale state survives an incident.
Make fallback defaults boring and observable
A fallback is a rollback boundary, so choose it from business invariants rather than from whichever Boolean makes the alert disappear. For the import gate, false might mean “use the established parser,” while true selects a newly deployed parser. That default is safe only if the established parser remains deployed, tested, and able to consume the current file format. If disabling the flag would skip imports entirely, false is not conservative at all.
Use a typed value and attach the evaluation reason to the run record. Never infer a Boolean from a string with generic truthiness: in Node.js, the non-empty string "false" is truthy. Validate environment variables and JSON at startup, then pass an actual Boolean to the evaluator. Also distinguish a missing key from invalid context, provider-not-ready, parse failure, and stale configuration. They can share a fallback value; they should not share an operational diagnosis.
The preventative path below lives in a small watchdog service. It accepts evaluation metadata produced by any application runtime, including Node.js, and makes the page depend on import freshness. The example compiles as Go and uses only the standard library.
package main
import (
"fmt"
"time"
)
type Evaluation struct {
Value bool
UsedDefault bool
Reason string
Revision string
}
type ImportResult struct {
RunID string
PropertyID string
Rows int
CursorMoved bool
CompletedAt time.Time
Evaluation Evaluation
}
func shouldPage(now time.Time, latest *ImportResult, maxAge time.Duration) (bool, string) {
if latest == nil {
return true, "no completed import result"
}
if now.Sub(latest.CompletedAt) > maxAge {
return true, "latest import result is stale"
}
if !latest.CursorMoved {
return true, "source cursor did not advance"
}
return false, "import output is fresh"
}
func main() {
now := time.Date(2026, 8, 16, 2, 10, 0, 0, time.UTC)
latest := &ImportResult{
RunID: "imp-20260816-0200",
PropertyID: "p-184",
Rows: 12481,
CursorMoved: true,
CompletedAt: now.Add(-3 * time.Minute),
Evaluation: Evaluation{
Value: false,
UsedDefault: true,
Reason: "FLAG_NOT_FOUND",
Revision: "cfg-9182",
},
}
page, reason := shouldPage(now, latest, 15*time.Minute)
fmt.Printf("page=%t reason=%q run=%s default=%t rows=%d\n",
page, reason, latest.RunID, latest.Evaluation.UsedDefault, latest.Rows)
}
Keep the error label low-cardinality. Put the flag key, run ID, and revision in logs or traces, not metric labels, unless the key set is tightly bounded. A useful counter groups reasons such as not_found, invalid_context, and provider_not_ready; the freshness gauge or completion timestamp drives the alert. This yields one signal for configuration hygiene and another for customer-facing work.
Short version: default locally, diagnose precisely, page on output.
Roll back code and configuration as separate changes
Delete and recreate is a destructive configuration migration, not an ordinary toggle. Treat it like a schema change: first make every active release tolerate absence, then remove targeting rules and references, observe through at least the relevant scheduling window, and only then delete. If the key must return, create and validate the new definition before any release depends on it. An immutable new key is often easier to reason about than pretending an old lifecycle continued, provided the naming and retention policy permit it.
Rollbacks become dangerous when an older binary still requires the deleted key. A code rollback can then restore a dependency that the current configuration no longer satisfies. Before deployment, test a small matrix: current code with current configuration, current code with the key absent, previous code with current configuration, and previous code with the intended fallback. Store the configuration revision beside the artifact version so the incident timeline can answer which pair actually ran. The catch is that this costs extra test cases and requires retaining a compatible path for at least one rollback window; teams that cannot preserve two paths should use a coordinated maintenance change rather than claim they have independent rollback safety.
There are three common response choices, and none wins everywhere:
| Response | Import availability | Detection quality | Rollback risk |
|---|---|---|---|
| Fail the job on a missing key | Low if the flag store is unavailable or the key is absent | Loud, but pages on configuration rather than output | High when older code still requires the key |
| Use a typed default and alert on every fallback | High | Good during rollout, noisy during a broad provider event | Moderate if the default path is retained and tested |
| Use a typed default, ticket fallback use, and page on stale output | High | Best aligned with the property import promise | Requires a trustworthy completion ledger and freshness window |
I prefer the third pattern for recoverable batch imports because it answers the pager question directly. It is not suitable when one incorrect evaluation can create an irreversible legal, safety, billing, or access-control action. In those systems, fail closed, require a human-approved configuration restoration, and accept reduced availability. Likewise, stick with a hard dependency when there is no safe old behavior to execute; inventing a fallback would conceal uncertainty rather than contain it.
What belongs in the postmortem?
The postmortem should reconstruct two timelines: configuration identity and import production. Include the delete event, recreation event, evaluated revisions, application deployments, cache or stream refreshes, fallback counts, completion records, cursor movement, alert evaluation, rollback decision, and recovery confirmation. Avoid the weak conclusion that “the flag was recreated.” The causal question is why an absence-tolerant contract, lifecycle guard, or output-based alert did not contain the mutation.
Assign actions to mechanisms. Add a deletion precondition that searches active references. Add a compatibility test for the previous release. Add evaluation-detail logging with redaction. Add a canary property whose import completion is checked before broad rollout. Review the alert's window against the real schedule and lateness budget. These are testable controls; “be more careful” isn't one.
No dashboard can substitute for that chain of evidence.
Sources
- OpenFeature specification, evaluation API and default-value behavior: https://openfeature.dev/specification/sections/flag-evaluation/
- OpenFeature specification, provider status and events: https://openfeature.dev/specification/sections/events/
- RFC 9110, HTTP Semantics, status code 404: https://www.rfc-editor.org/rfc/rfc9110.html#name-404-not-found
- Node.js documentation, environment variables: https://nodejs.org/api/environment_variables.html
- Google SRE Workbook, alerting on symptoms: https://sre.google/workbook/alerting-on-slos/
Top comments (0)