Skip to content
Preptima
hardDesignScenarioMidSeniorStaff

Where does train-serve skew come from, and how do you stop it?

Skew is any difference between the feature values a model trained on and the ones it receives at request time. It comes from two implementations of one transformation, joins that ignore the as-of timestamp, and features unavailable in a request. Prevent it with shared code, as-of joins and served-vector logging.

5 min readUpdated 2026-07-28
Practice answering out loud

What the interviewer is scoring

  • Whether the candidate insists on one implementation of the transformation rather than two that are kept in step by review
  • Does the answer distinguish a code difference from a timing difference, since the fixes are unrelated
  • That logging the served feature vector is proposed as infrastructure rather than as a debugging afterthought
  • Whether feature availability at request time is checked before the feature is engineered
  • Can the candidate describe a monitor that would have caught the skew automatically

Answer

Two different failures wearing one name

Train-serve skew is any systematic difference between the feature values used to fit a model and the values the model receives in production. It splits into two kinds that share nothing except the symptom, and separating them early is most of the diagnostic value.

A code difference means the same conceptual feature is computed by two different pieces of logic. The training pipeline is SQL against a warehouse; the serving path is application code in a request handler. They were written from the same specification by different people at different times, and they disagree about something small. Nulls become zero in one and the column mean in the other. One trims whitespace before matching a category, the other does not. One computes a ratio guarding against a zero denominator by returning zero, the other by returning one.

A timing difference means the logic agrees and the clock does not. The training row was assembled with data available at some later moment than the decision it represents, or the serving path reads a cache whose contents lag behind what the warehouse would have shown. This kind is more dangerous because both implementations are individually defensible and there is nothing to find in a code review.

The code difference has exactly one real fix

Every mitigation short of sharing the implementation is a promise to stay disciplined, and those decay. The fix is that both paths call the same transformation code, so there is one definition and it cannot drift.

In practice that is a library the training job imports and the serving process also imports, a serialised pipeline artefact fitted at training time and loaded at inference, or a transformation graph exported alongside the model. Which mechanism matters far less than the property: a change to the feature is a change in one place, and a model artefact carries the exact fitted parameters it was trained with rather than recomputing them from whatever data serving happens to see.

# features.py, imported by BOTH the training job and the request handler.
def order_features(orders, as_of):
    window = orders[(orders.ts < as_of) & (orders.ts >= as_of - DAYS_7)]
    return {
        "orders_7d": len(window),
        # Explicit: no orders means zero, and that decision lives here only.
        "mean_value_7d": window.value.mean() if len(window) else 0.0,
    }

The as_of parameter is doing the important work in that signature. A function that cannot be asked "as of when" will be called with the current time in production and with whatever the warehouse contains in training, and no amount of shared code prevents the resulting skew.

flowchart LR
    A[Event log<br/>with timestamps] --> B[Shared transform<br/>as-of aware]
    C[Online store<br/>latest values] --> B
    B --> D[Training rows<br/>as of label time]
    B --> E[Served vector<br/>as of request time]
    E --> F[Prediction log<br/>features kept]
    F --> G[Skew monitor<br/>and next training set]
    G --> D

The loop from the prediction log back into the training set is the part most designs omit, and it is what turns skew from an invisible defect into a measurable one.

Point-in-time correctness is a property of the join

The timing failure is prevented by never joining a feature table on the entity key alone. Every join must also constrain the feature's own timestamp to be at or before the row's decision time, and to be the latest such value.

Take a churn label determined at 14:00 on 3 March. A plain key join against a customer_profile snapshot table picks up whichever snapshot the table holds, quite possibly the one written on 4 March, which already reflects the customer having phoned to cancel. The support_tickets_open count in that snapshot is one, and it was zero at the moment the prediction would have been made. Repeated across the training set, the model learns that open tickets predict churn with a strength no live system can reproduce, because live it sees the count before the call rather than after it.

The as-of join costs real complexity. You need event-level history rather than snapshots, or a slowly-changing dimension with validity intervals, and the join is a window function rather than an equality. That cost is the reason feature stores exist as a category, and it is also the reason a small team can reasonably decide to keep dated snapshots and join on the snapshot date explicitly instead. What is not reasonable is joining on the key and hoping.

Features that are cheap offline and impossible online

The third source is a feature that was never servable. In a warehouse you can compute an entity's 90-day aggregate over a billion rows in a minute and nobody minds. In a request handler with a 50 millisecond budget you cannot, so somebody substitutes a nightly-refreshed approximation, or an hourly cache, or the feature quietly resolves to null.

Ask the availability question before engineering the feature rather than after. For each candidate, what is the latency to compute it at request time, what is the freshness of the source it reads, and what value appears if the source is unreachable. A feature whose answer is "computed nightly" is legitimate as long as the training pipeline also computes it as of the previous night rather than as of the label moment. The skew is not caused by staleness, it is caused by asymmetric staleness.

The degradation path needs the same treatment. If the online store times out and the handler substitutes a default, the model is now scoring a vector it never saw in training. Either train with those defaults present at their realistic rate, or make the fallback an explicit missing indicator the model was fitted with, or fail the request. Silently substituting zero is the worst of the three and the most common.

Instrumentation is what makes this tractable

Skew is invisible without logging, so log the exact feature vector that was served alongside the prediction and the entity and timestamp. That log pays for itself three times over. It lets you replay production rows through the offline pipeline and diff feature by feature, which localises a skew bug to a column in minutes. It gives you a training set assembled from what serving actually produces, which eliminates the whole class of problem for retraining. And it lets you run a standing monitor that compares the distribution of each feature as served against the same feature as computed offline, alerting on a shift in a summary statistic rather than waiting for the business metric to move.

The failure worth naming is the belief that a careful review of both code paths is sufficient. It is not, because the paths are maintained on different release cadences by different teams, and the divergence is introduced later by a change that looked local. Skew is a structural problem about having two sources of truth, and structural problems are not fixed by attentiveness.

If a feature is computed twice it will eventually be computed differently. Share the transformation, join on the as-of timestamp rather than the key, and log the vector you served so the next investigation is a diff instead of an archaeology project.

Likely follow-ups

  • How do you detect skew on a feature that is legitimately different offline, such as one built from a nightly batch?
  • What does point-in-time correctness cost you in pipeline complexity, and when is it not worth it?
  • A feature takes 400 milliseconds to compute and your latency budget is 50. What are the options?
  • How would you backfill a new feature so that training data matches what serving will produce?

Related questions

train-serve-skewfeature-storepoint-in-timedata-leakagemlops