Monitoring in production: alerts, delayed labels, feature stores and skew
Before this lesson: lesson 07 (PSI, KS and the kinds of shift); lesson 05 (the serving path that reads features); lesson 06 (rollback and retraining, the answers to an alert); multiple testing. Time: about 60 minutes, plus the notebook.
Why this matters #
After a release, a model can fail in two quiet ways. The world changes (drift, lesson 07), or the pipeline feeds the model wrong inputs (skew). Both look the same from outside: the server answers, and the answers are worse. A monitor that misses them costs money or trust; a monitor that pages too often is ignored, which is just as bad.
Weather and climate ML has the same problems. ML weather models are trained on reanalysis data (ERA5) but run from operational analyses, whose statistics differ. Observation labels (rain gauges, river levels) arrive after the forecast time. Fraud labels (chargebacks) arrive weeks later. "How would you monitor this model?" and "What is training-serving skew?" are standard questions in ML system design interviews.
1. Predict first #
2. The picture #
Layers of a monitor. Watch the system from the outside in. Each layer catches problems the layers above it cannot.
| Layer | Example signals | Labels? | Catches |
|---|---|---|---|
| service health | request rate, errors, p99 latency | no | outages, overload |
| data quality | schema, nulls, ranges, volume, freshness, feature age | no | broken pipelines, unit changes, stale features |
| input drift | PSI or per feature, a domain classifier | no | covariate shift and label shift |
| prediction drift | distribution of or of the score | no | input changes that reach the output |
| performance per slice | accuracy, calibration per station, region or class | yes, delayed | everything, including concept drift |
Every alert needs an owner and a runbook: a short list of what to check and what to do (check the upstream data, roll back, retrain; lesson 06).
A score over time. A drift monitor computes a score (here PSI against a reference) on the last values, every new values. The window sets how much data each score sees; the stride sets how often a score is computed. Then a rule decides when to page.
Switch between naive rule and hysteresis, then try the other streams. Top: the stream and its true mean (orange). Bottom: the rolling PSI (orange), the warn, alert and clear levels (aqua) and the pages (magenta). Notice: in "near the threshold" the score crosses the alert level many times during one incident, so the naive rule pages 5 times and hysteresis once. In "no drift", make the window small: the noise floor of lesson 07 rises and false alarms appear. In "seasonal", every season looks like drift because the reference holds one season only.
Warn 0.10, alert 0.25, clear 0.10. The PSI per window is 0.08, 0.26, 0.24, 0.27, 0.15, 0.26, 0.07, 0.30. How many pages does the naive rule send ("page each time the score crosses 0.25 from below"), and how many alert pages does hysteresis send?
The naive rule pages at windows 2, 4, 6 and 8 (each time after a value below 0.25). With hysteresis, window 2 moves the state to alert; 0.24, 0.27, 0.15 and 0.26 are all at least 0.10, so the alert is never cleared and no new page is sent. Window 7 (0.07 < 0.10) clears it ("recovered"), and window 8 is a new incident: a second page.
3. Definitions and main results #
Designing a drift monitor #
- Windows. Score the last values every new values. A short window reacts fast but is noisy (its PSI noise floor is high); a long window is stable but slow. Use sliding windows, not cumulative statistics: a cumulative accuracy over months hides a sudden dip.
- Reference. Usually the training data. For seasonal inputs (temperature, river flow) use the same season of earlier years, or every summer looks like drift.
- Thresholds. Calibrate them on stable history: replay months without a known incident and take a high quantile (for example 99.9%) of the window scores. Prefer effect sizes (PSI, ) to p-values (lesson 07).
- Levels and hysteresis. Use two levels, warn and alert, and a lower clear level.
Definition (alert state machine with hysteresis). The state is ok, warn or alert, and . For each new score :
- Compute the level of : alert if , warn if , else ok.
- If this level is higher than the state, move up to it and send an event ("warn" or "alert").
- Otherwise, if the state is not ok and , move down to ok and send "recovered".
- Otherwise, stay and send nothing.
So a state goes up at once, but it comes down only below the clear level. One incident gives one page, even when the score jumps around the alert threshold. A persistence rule ("page only after 3 windows in a row above alert") is another way to get the same effect, at the cost of a later page.
Many features. Each check of a feature has a small false-alarm probability .
Result. With independent features, the expected number of false alarms per check is , and the probability of at least one is .
Proof. Each feature gives a false alarm with probability ; the count is , with mean . No false alarm at all means every feature is quiet, which has probability by independence.
With 200 features and : 2 false alarms per check on average, and , so 87% of checks page someone for nothing. (Correlated features give fewer, but still many.) So page on a few signals (prediction drift, data quality, the most important features), and show the rest on a dashboard. Alerts that nobody acts on teach people to ignore all alerts: this is alert fatigue.
You monitor 200 independent features. Each check of one feature has a 1% false-alarm rate. What is the probability that a check of all features pages someone although nothing changed?
The probability that all 200 are quiet is , so at least one fires with probability . 2 is the expected number of false alarms per check, not a probability. To fix it, page on few signals or lower each feature's rate (for example , the Bonferroni correction).
Delayed labels #
A performance metric needs labels. If the label of a prediction made at time arrives at time , then at time the metric can only use predictions with . With a window of length , the monitor at time uses the predictions made in
Result. Let traffic be uniform, and let the true accuracy change from to at time . Then the monitored accuracy is for , it is for , and in between it is
Proof.
- The usable predictions lie in , an interval of length .
- If , all of them were made before the change: the metric is .
- If , all of them were made after the change: the metric is .
- In between, the part after is , of length . With uniform traffic the metric is the weighted average of and with weight on .
Answer to question 2 of section 1. With , and , the monitored accuracy starts to fall after day 130 and shows the full drop of 0.60 from day 160. At day 145 half of the usable predictions come after the drop, so it reads .
Move the label delay and the window, then switch the cause to concept drift. Blue is the true accuracy, orange what the accuracy monitor can measure, violet the PSI of each day's inputs, aqua the alert levels and magenta the first alert of each monitor. Notice: the accuracy alert comes about days after the drop or later; the input monitor reacts on day 100. With concept drift the inputs do not change, so only the late accuracy monitor sees the problem. Make the window shorter than the delay: the accuracy monitor has no data at all.
Two more facts about delayed labels:
- The evaluation window must be longer than the label delay, and a quality drop shows up only after the delay. This is the main reason to also monitor inputs and predictions.
- Labels that arrive early are often not a random sample. Obvious fraud is reported first; a flood is confirmed before a "no flood". An accuracy computed on early labels is biased. Report the share of labels that have arrived next to the metric.
A flood model's labels arrive 30 days late. On the first day of a problem, which monitors can react?
Service health, data quality, input drift and prediction drift need no labels, so they react at once (if the problem changes the inputs). Accuracy and calibration need labels and react only after 30 days. Pure concept drift is visible only there.
Feature stores #
A feature is a value computed for an entity (a user, a station, a grid cell) at an event time, for example "soil moisture of station S1 at 12:00". A feature store holds each feature definition once and provides its values both for training and for serving. Its parts:
- The offline store keeps the full history of feature values with timestamps (a data warehouse or Parquet files). It builds training sets and runs batch scoring.
- The online store keeps only the latest value per entity in a low-latency key-value store. The serving API reads it at request time, in milliseconds.
- Materialisation is the scheduled job that loads the latest values from the offline store (or a batch source) into the online store. Streaming features are pushed into the online store directly.
- The TTL (time to live) of a feature is the maximum age of a valid value. Offline, the training-set join ignores values older than the TTL. Online, an older value should count as missing too, so that both paths agree. Stores differ in whether they enforce this online, so check your store, or enforce the age limit in the serving code.
Feast, Hopsworks and the feature stores of the large cloud platforms all have these parts.
Point-in-time correct joins #
To train, we join each label row (entity , time , label ) with feature values. The values must be the ones the model could have known at time .
Definition (point-in-time correct join). A label row gets the feature row of entity with the latest time such that
- (strict), or (inclusive), and
- (the value is not too old).
If there is no such row, the features are missing.
- Strict or inclusive? Use strict when a value stamped at can contain the label
event itself (for example a count of transactions that includes the current one). Inclusive
is right when values stamped at were really available before the prediction.
pd.merge_asofis inclusive by default;allow_exact_matches=Falsemakes it strict. - TTL. Feast's historical join scans backward from each label time for at most one TTL; the TTL is relative to each label time, not to the time you run the query.
- The wrong join. A plain join on the entity with today's feature table gives old label rows values from their future. This is future leakage: offline metrics become too good, and online the model meets other inputs. Rule #31 of Google's Rules of ML warns about exactly this: a table you join at training time may have changed by serving time.
Move the TTL, switch strict and inclusive, then switch to the plain join. Blue dots are the feature rows of station S1, orange dots the label rows, aqua bands each label's look-back window , and a line shows which feature row each label gets. Notice: the label at 12:00 gets 0.25 only with the inclusive rule. With the plain join every label gets 0.40, and the three labels before 18:00 see a value from their future (magenta).
Feature rows for station S1: 08:00 (value 3) and 09:30 (value 7). A label row is at 09:30, the TTL is 1 hour, and the join is strict (). What does the label row get?
Strict excludes the row at 09:30. The latest earlier row is 08:00, which is 1.5 hours old, more than the TTL of 1 hour. So no valid row exists and the feature is missing. With the inclusive rule () the row would get 7.
Training-serving skew #
Google's Rules of ML define training-serving skew as a difference between performance during training and performance during serving, caused by a difference in how the two pipelines handle data, a change in the data between training and serving, or a feedback loop. This lesson uses the word skew for the pipeline cause: the same example gets different inputs offline and online. A change of the world is drift (lesson 07).
| Cause | Mitigation |
|---|---|
| two code paths (SQL for training, another language at serving) | one feature definition for both paths: a feature store or a shared library (rule #32: re-use code between training and serving) |
| future leakage: features computed after the label time | point-in-time joins on event time (rule #31) |
| stale or missing online features (materialisation lag, expired TTL, other default values) | monitor feature age and null rate online; the same TTL and the same default on both paths |
| client-side preprocessing (the app normalises before calling the API) | move preprocessing into the model artifact or the server; validate inputs at the API |
| different library versions (tokenizer, scaler, pandas), float32 vs float64 | one pinned image or lockfile for training and serving (lesson 03); a parity test |
How to find skew. Log the features actually used at serving time (rule #29: "save the set of features used at serving time"). For the same entity and time, recompute the features with the training pipeline. Compare them feature by feature: report the match rate per feature (exact, or within a stated tolerance), then compare the model scores. This is rule #37: measure training-serving skew.
Choose a cause of skew. Each dot is one logged request: the value the training pipeline computes (x) against the value the server used (y). Blue dots match, magenta dots do not; the dashed line is online = offline. Notice: client-side scaling breaks every humidity value; a stale value breaks a third of the temperatures; float32 breaks most values under the exact test and none under a small tolerance, so state the tolerance with the match rate.
Skew or drift? Skew: the same example, different inputs; fix the pipeline. Drift: the world changed; monitor, retrain or adapt. To decide, take logged serving features and recompute the training features for the same entities and times. If they differ, it is skew. If they match but the production distribution differs from the training set, it is drift.
Answer to question 1 of section 1. Future leakage. Label rows from before a feature update see the later value, so the offline metrics are too optimistic, and online the inputs differ from the training inputs. Input drift monitors compare marginal distributions of production with training, so they can miss it. The fix is a point-in-time join.
Offline AUC is 0.91, online the model behaves like AUC 0.80, and no input feature shows drift. What do you check first?
"No drift but worse online" points to skew: the same example gets other inputs online than offline (for example future leakage in the training join, or a second code path). A parity check on logged features finds it, feature by feature. Retraining on a buggy pipeline keeps the bug.
When accuracy drops: a debugging order #
Check the cheap causes first:
- Service and data quality. Errors, timeouts, nulls, unit changes, stale features.
- Skew. Logged serving features against recomputed training features.
- Drift per feature and per slice. Which features moved, for which stations or regions.
- Concept drift. Fresh labels: is the rule itself different? Then retrain on recent data or roll back (lesson 06).
4. Worked example #
A. Hysteresis. Warn 0.10, alert 0.25, clear 0.10. PSI per window:
| Window | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| PSI | 0.05 | 0.12 | 0.27 | 0.22 | 0.26 | 0.24 | 0.08 | 0.05 |
| level of the score | ok | warn | alert | warn | alert | warn | ok | ok |
| state after the step | ok | warn | alert | alert | alert | alert | ok | ok |
| event | — | warn | alert | — | — | — | recovered | — |
Step by step: window 2 (0.12) is above warn, so the state moves up to warn and sends "warn". Window 3 (0.27) moves it up to alert: one page. Window 4 (0.22) has level warn, which is not higher than the state, and (clear), so nothing happens. Windows 5 and 6 are the same. Window 7 (0.08 < 0.10) clears the alert: "recovered". A naive rule "page when the score crosses 0.25" pages at windows 3 and 5: two pages for one incident.
B. Point-in-time join. Station S1 has soil_moisture at 06:00 (0.21), 12:00 (0.25) and 18:00
(0.40). Label rows at 05:00, 12:00, 13:00, 20:00 and 23:30. Strict , TTL 4 hours.
| Label time | Latest feature row with | Age | Value |
|---|---|---|---|
| 05:00 | none | — | missing |
| 12:00 | 06:00 (12:00 is not strictly before) | 6 h > 4 h | missing |
| 13:00 | 12:00 | 1 h | 0.25 |
| 20:00 | 18:00 | 2 h | 0.40 |
| 23:30 | 18:00 | 5.5 h > 4 h | missing |
With the inclusive rule, the label at 12:00 gets 0.25 (age 0). A plain join on the station with the current table gives 0.40 to every row: the rows at 05:00, 12:00 and 13:00 see a value from their future.
C. Delayed labels. Labels arrive days after each prediction, and the accuracy window is days. At day the metric uses the predictions of . The true accuracy drops from 0.90 to 0.60 at day 100.
- Day 130: the usable predictions are from , all before the drop: 0.90.
- Day 145: ; the part after the drop is , half of 30 days: .
- Day 160: , all after the drop: 0.60.
An input drift monitor can react on day 100, if the drop comes with an input change.
5. In code #
The code checks the three worked examples; the notebook asks you to write each function yourself.
import numpy as np
import pandas as pd
# A. The alert state machine with hysteresis.
ORDER = {"ok": 0, "warn": 1, "alert": 2}
def step(state, score, warn=0.10, alert=0.25, clear=0.10):
level = "alert" if score >= alert else "warn" if score >= warn else "ok"
if ORDER[level] > ORDER[state]:
return level, level # move up, send an event
if state != "ok" and score < clear:
return "ok", "recovered" # move down only below clear
return state, None
state, events = "ok", []
for score in [0.05, 0.12, 0.27, 0.22, 0.26, 0.24, 0.08, 0.05]:
state, ev = step(state, score)
events.append(ev or "-")
print(events) # ['-', 'warn', 'alert', '-', '-', '-', 'recovered', '-']
# Many features: chance of at least one false alarm per check.
print(f"{1 - 0.99**200:.3f}") # 0.866
# B. Point-in-time join with a 4-hour TTL, strict s < t.
t = lambda h: pd.Timestamp("2026-07-01") + pd.Timedelta(hours=h)
features = pd.DataFrame({"station": "S1", "time": [t(6), t(12), t(18)],
"soil_moisture": [0.21, 0.25, 0.40]})
labels = pd.DataFrame({"station": "S1", "time": [t(5), t(12), t(13), t(20), t(23.5)]})
joined = pd.merge_asof(labels, features.assign(feature_time=features["time"]),
on="time", by="station", direction="backward",
tolerance=pd.Timedelta(hours=4), allow_exact_matches=False)
print(joined["soil_moisture"].tolist()) # [nan, nan, 0.25, 0.4, nan]
# C. Delayed labels: delay 30 days, window 60 days, accuracy 0.90 -> 0.60 on day 100.
day = np.arange(0, 250, 0.01) # prediction times (uniform traffic)
acc = np.where(day < 100, 0.90, 0.60)
def monitored(t_now, delay=30, window=60):
use = (day > t_now - window) & (day <= t_now - delay) # in the window and labelled
return acc[use].mean()
print(", ".join(f"{monitored(t_now):.2f}" for t_now in (130, 145, 160))) # 0.90, 0.75, 0.60
merge_asof needs both tables sorted by time. Its tolerance is inclusive ( TTL), which
matches in the definition.
6. Where it shows up #
- ML weather models. They are trained on ERA5 reanalysis but run from operational analyses with other statistics: the same atmospheric state gets other inputs in training and in operation, which is skew in the sense of this lesson. For this reason, the versions of Google DeepMind's weather models used operationally are fine-tuned on ECMWF HRES data and start from operational HRES initial conditions (GraphCast repository, as of 2026). An upgrade of the data assimilation system, or a new satellite instrument, shifts the input statistics overnight.
- Hydrology and observations. River levels and rain-gauge data arrive late and are quality controlled later still: a delayed-label problem. Monitor per station, the natural slice.
- Fraud and credit. Chargebacks arrive weeks later, and the early ones are the obvious cases. Feature stores were built for these systems: many features per user, computed in batch for training and read online in milliseconds.
- Recommendation and ranking. Features such as "clicks in the last hour" are pushed by streaming jobs; materialisation lag and different default values are common sources of skew.
- Tools. Metrics and alerts are often kept in Prometheus and Grafana; drift reports in Evidently or NannyML; feature stores in Feast, Hopsworks or a cloud platform. The concepts of this lesson are the same in all of them.
7. Common mistakes #
- Paging on every feature. Many features times a small false-alarm rate gives a page on most checks.
- A single threshold without hysteresis. One incident around the threshold gives many pages.
- Cumulative statistics. They average a sudden incident away. Use sliding windows.
- The training data as the reference for a seasonal feature. Every season then looks like drift.
- Thresholds chosen by hand. Calibrate them on stable history, for the window size you use.
- Trusting accuracy on early labels. Early labels are not a random sample, and the metric lags by the label delay.
- Building training data with a plain join on the current feature table. Future leakage and skew.
- Retraining before checking skew. A buggy pipeline stays buggy after retraining.
8. Check yourself #
- Why do monitors use sliding windows instead of a cumulative statistic since the release?
Answer
A cumulative statistic averages over all data since the release, so a sudden incident moves it very little and very late. A sliding window of length forgets old data and reacts within about values; the stride sets how often it is checked. - How do you choose the alert threshold of a PSI monitor?
Answer
Replay a stable period (no known incident) with the same window, stride and binning, compute the window scores, and take a high quantile, for example the 99.9% quantile. The threshold then has a known false-alarm rate per window. It must be recalibrated if the window or the binning changes. - Labels arrive 14 days late and the accuracy window is 7 days. What does the accuracy monitor show?
Answer
Nothing: the usable predictions are in , which is empty. The window must be longer than the delay, for example 28 days, and then a drop shows fully only 28 days after it starts. - A feature has rows at 08:00 (value 3) and 09:30 (value 7); a label row is at 09:30; TTL 1 hour.
What does the label row get with ? When is that right?
Answer
7 (age 0). It is right if the value stamped 09:30 was really available before the prediction at 09:30 and does not contain the labelled event. If the value is computed from events up to and including the labelled one, use strict , which gives "missing" here. - Offline AUC 0.91, online the model behaves like 0.80. How do you decide between skew and drift?
Answer
Take logged serving features (rule #29) and recompute the training features for the same entities and timestamps. If they differ for the same example (a low match rate on some feature), it is skew: fix the pipeline. If they match but the production distributions differ from the training set, it is drift: retrain or adapt. - The online store's materialisation job fails for 6 hours. Which monitor sees it first, and how
would the model's inputs change?
Answer
A data-quality monitor on feature age (freshness) sees it at once. The online store keeps serving the last materialised values, so the model gets stale inputs. If the serving code enforces the TTL, the values become missing after one TTL and the null rate rises. Drift monitors may see it later, or not at all if the stale values look normal.
What is hysteresis in an alerting rule?
The alert state is entered at the alert level but left only below a lower clear level, so a score that wobbles around the threshold gives one page, not many.
Why use sliding windows instead of cumulative statistics?
A cumulative statistic averages a sudden incident away; a sliding window forgets old data and reacts within about one window.
How do you calibrate a drift alert threshold?
Replay stable history with the same window and binning, and take a high quantile (for example 99.9%) of the window scores.
200 independent features, 1% false-alarm rate each: probability of at least one false alarm per check?
.
With label delay and window , which predictions can an accuracy monitor use at time ?
Those made in ; none if .
What is the difference between the offline and the online feature store?
The offline store keeps the full timestamped history for training and batch scoring; the online store keeps the latest value per entity for low-latency serving.
What is materialisation in a feature store?
The scheduled job that copies the latest feature values from the offline store (or a batch source) into the online store.
State the point-in-time join rule for a label row at time .
Take the entity's feature row with the latest time such that (or ) and ; if none exists, the feature is missing.
Define training-serving skew (in the pipeline sense).
The same example gets different feature values offline (training pipeline) and online (serving).
Skew or drift: which one does retraining fix?
Drift (the world changed). Skew is a pipeline bug; retraining on the same pipeline keeps it.
9. Interview angle #
- "How would you monitor a model whose labels arrive weeks later?" — Layers: service health, data quality (including feature freshness), input and prediction drift with calibrated thresholds and hysteresis, then delayed performance per slice with the share of arrived labels; a runbook per alert.
- "How do you avoid alert fatigue?" — Page on few signals; calibrate thresholds on stable history; hysteresis or persistence; everything else on a dashboard; an owner for every alert.
- "What is training-serving skew, and how does a feature store help?" — The same example gets different inputs; one feature definition for both paths, point-in-time joins, the same TTL and defaults; log serving features and measure the match rate.
- "Accuracy dropped. How do you find out why?" — Data quality first, then skew (logged vs recomputed features), then drift per feature and slice, then concept drift with fresh labels; then roll back or retrain.
10. Practice #
The notebook for this lesson runs in Colab. Each exercise has a check cell and a folded solution:
- Write
rolling_scores(reference, stream, window, stride, metric)with the PSI and KS functions of lesson 07 (given in the notebook). - Write
drift_state_step(state, score, warn, alert, clear)and reproduce the hysteresis table of worked example A; count pages of the naive rule and of hysteresis on a noisy stream. - Write
labeled_accuracy(t, pred_times, label_delays, correct, window)and reproduce worked example C (0.75 at day 145). - Write
point_in_time_join(labels, features, ttl, strict)with pandas, withoutmerge_asof, and compare it withmerge_asof. - Skew hunt: a training feature function and a serving function with a planted bug; compute the match rate per feature on logged requests and find the bug.
11. Watch and read #
- Google, Rules of Machine Learning: the section "Training-Serving Skew", rules #29 to #37.
- Feast documentation, Point-in-time joins: the TTL example.
- Chip Huyen, Data distribution shifts and monitoring (2022): the section "Monitoring and Observability".
- Made With ML, Monitoring: the sections on delayed outcomes, drift and solutions.
- Breck et al., Data Validation for Machine Learning (SysML 2019): validation between batches and of training against serving data.
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.