TL;DR — A model is a total function: it returns a plausible answer for almost any input, including broken ones. That makes upstream data outages invisible to conventional monitoring. The fix is to treat imputation and fallback rates as first-class SLIs, carry provenance for every feature value, and give the serving path an explicit abstain option.

Ordinary software fails loudly. Pass a null where a parser expects a string and you get a stack trace, an alert, and a ticket. Machine learning systems have no such reflex. A model is a total function. Hand it a vector, any vector, and it hands back a number.

That is the root of most data quality incidents I have seen in production ML. The data was wrong, nothing threw, and the model answered anyway. My thesis: the robustness we build into models and feature layers is exactly what makes data failures invisible. Observability for ML starts by undoing some of that robustness on purpose.

The Outage That Looks Like Normal Traffic

Here is a pattern worth knowing by heart. A feature lookup depends on an upstream service. The service gets slow, the lookup times out, and the client library, reasonably, returns a configured default. The model receives a zero where it expected a rolling average of recent activity.

A gradient-boosted tree takes the branch it learned for that value. A neural net computes a perfectly finite logit. Latency is fine, error rate is zero, and the score distribution moves a little. Nobody pages anyone, because nothing in the request path was an error.

Every layer behaved correctly in isolation. The client's default was documented. The model handled the value because training data contained missingness too. The serving layer returned a valid response. The failure exists only in the composition, and no single component owns it.

Graceful Degradation Destroys Information at the Boundary

Each fallback is a lossy conversion of an error into a value. Once the conversion happens, the downstream system cannot tell "this user has no history" from "we failed to fetch this user's history." Those are different facts with different correct behaviors, and they now share the same bytes.

Aircraft instrumentation gets this right. A failed sensor does not display zero altitude. It displays a flag, and pilots are trained to treat the flag as information. A feature pipeline that substitutes a default for a failed read is the equivalent of painting zero on the altimeter.

Three common mechanisms do this conversion:

  • Client-side defaults in feature-store SDKs that swallow timeouts.

  • Imputation steps baked into the model's preprocessing, which run identically whether the value was missing for a legitimate reason or an infrastructural one.

  • Stale reads, where a cache or a lagging materialization returns an old but well-formed value. This is the hardest to catch because nothing is even missing.

Schema validation and null-rate checks on the training table do not see any of this. They validate the data at rest. The damage happens at read time, per request, in the serving path.

Instrument Provenance, Not Just Values

The fix is not more checks on columns. It is carrying a small amount of metadata alongside every feature value so the serving layer can say how it got that value. At minimum, each value should be tagged as one of:

  • observed: fetched fresh from the source of truth.

  • stale: fetched, but older than its freshness budget.

  • imputed: filled by a deliberate, learned or configured rule.

  • defaulted: filled because a read failed.

From those tags you can compute the metrics that matter. The most useful is the fallback rate per feature, per segment. Freshness at read time is the next one, not freshness at write time. A table that was updated on schedule can still be served from a replica that is hours behind.

Segmentation is not optional. Aggregate fallback rates are dominated by the majority cohort, and outages are rarely uniform. A regional dependency fails and one geography gets defaulted features while the global number barely moves. Slice by whatever dimension your infrastructure fails along: region, tenant, entity type, upstream source.

You also need a baseline that separates expected missingness from induced missingness. New users legitimately have no history. The signal is the fallback rate conditioned on cohort, compared against that cohort's normal level. An alert on "null rate above X" is noisy. An alert on "defaulted rate for established users departed from its own history" is not.

Give the Serving Path a Way to Say No

Observability that only records the problem is a postmortem tool. The stronger move is to make the serving path act on provenance. Instead of a binary serve-or-error, define three outcomes:

  • Serve: inputs are within contract, use the primary model.

  • Degrade: some features are defaulted or stale beyond a threshold, so route to a simpler model trained to not depend on them, or to a rules baseline.

  • Abstain: too much of the input is untrustworthy, so return an explicit "no prediction" that the caller must handle.

This is a circuit breaker, applied to data instead of dependencies. The key design choice is that the degrade path must be a model you trained and evaluated for that regime, not an accident. A model trained with random feature dropout can handle a missing feature on purpose. A model that merely tolerates one is guessing.

Abstain is the uncomfortable one, because product teams read it as downtime. But a confident wrong score is usually more expensive than an honest absence, especially when the score feeds another automated decision. Downstream systems can only behave sensibly if the abstention is visible to them.

The Same Failure Wears an LLM Costume

This is not just a tabular-model problem. In a RAG system, a retrieval failure that returns zero documents does not stop the model. It answers fluently from parametric memory, and the response looks identical in shape to a grounded one. An agent whose tool returns an empty list will often continue as if the empty list were a finding.

The same discipline applies. Log whether context was retrieved, how much, and how fresh. Track the empty-context rate and the tool-returned-nothing rate as SLIs. Give the generation step an abstain path ("I could not find supporting material") that the pipeline enforces rather than hopes for. Fluency is the LLM equivalent of a finite logit: it tells you the function ran, not that the input was sound.

What to Do Monday

Start with an audit, not a platform. Walk the serving path for one model and list every place an error can become a value: SDK defaults, preprocessing imputers, cache fallbacks, try/except blocks that return a constant. Each one is a silent failure site.

Then, in order:

  • Emit a provenance tag for every feature on every request, even if you only sample the logs.

  • Dashboard the defaulted and stale rate by cohort next to your latency and error panels.

  • Set a budget, and treat breaching it as an incident even when accuracy proxies look fine.

  • Train the fallback model deliberately, then wire the breaker to it.

Models are built to be forgiving, and that is a virtue in the weights and a vice in the system. If the system never complains, it is not healthy. It just has no way to tell you.