Feature Engineering in Production
Online vs offline features, point-in-time joins, backfill risk
A fraud model ships with 94% offline AUC. In production it scores 82%. No code changed. No error fired. The model is even getting the exact feature it trained on — "number of transactions in the last 7 days" — but the *value* is wrong.
At training time that number came from a SQL query over a history table: an exact count. In production it comes from a Redis structure that counts approximately to stay fast. For most users the two agree. For high-volume accounts — the very users most likely to be fraud — the approximation drifts furthest from the true count, since probabilistic counters like HyperLogLog trade precision for speed exactly where volume is highest. The model was taught to trust these counts as exact, and now it is scoring live traffic on values it never learned from. Twelve points of accuracy, gone. Not a model failure — an infrastructure failure.
The same five culprits, in feature terms
*Language:* the Python training code and the Java serving code implement one formula two ways, and the small differences add up. *Data source:* batch SQL and real-time Redis handle nulls and completeness differently. *Timestamp:* training computes the feature at label time, serving computes it at request time hours later — for a rolling 7-day window that gap matters. *Preprocessing:* a scaler saved under one library version can load differently under a newer one, shifting every value silently. *Leakage:* a training feature used data that won't exist at serving time, inflating the offline score.
One shared computation path removes four of the five at once — language, data source, timestamp, and preprocessing all collapse into a single well-tested implementation: a single library or service computes "transactions in last 7 days" identically whether the caller is the training pipeline or the live endpoint. Leakage needs one more discipline on top of that: even a single shared function will leak if it's called with a wrong or late as-of timestamp, so point-in-time join enforcement is required in addition to the shared path, not replaced by it.
Point-in-time joins: the rule that keeps training honest
Here is the subtle one. When you build a training row for a fraud event at time T, you must join only feature values that existed *before* T. Grab a value from T + 1 hour — easy to do by accident, because the training job ran later — and you've leaked the future into the past. For a rolling 7-day window computed an hour late, the count can include transactions that happened *after* the event you're trying to predict. The model learns a pattern built partly on future data. In production it never has the future, so the offline number is a mirage and production comes in lower.
One warning worth internalizing. "It's the same logic, just in another language" is a hope, not a fact — it's only true once you've tested that both produce byte-identical output on identical input. Re-implementing SQL window logic in Java quietly changes null handling, overflow, and rounding, and on high-value accounts those small differences can outweigh the model's entire learned signal for that user.
Key points
- Build one feature computation path shared by training and serving, even if it means running Python at serving time or invoking a service from the training pipeline. The cost of maintaining two codepaths is not the engineering time to write them — it is the silent accuracy degradation that accumulates once they diverge. Every language boundary, data source switch, and library version difference is a potential skew source. One path eliminates the language, data source, timestamp, and preprocessing sources of skew structurally — leakage still needs point-in-time join enforcement on top of it (see the trap below).
- Trap: point-in-time join mistakes are a common, easy-to-miss leakage source in production ML. When constructing a training row for an event at time T, join only features that were computed and available before T. Using a feature from after T — even by one second — is leakage. The model learns a relationship that includes future information. In production it never has that information. Offline AUC looks great; production performance is worse than expected, and you spend weeks debugging the wrong things.
- Diagnostic: run both pipelines on the same 1,000 held-out examples and compute mean absolute difference per feature. Any feature with MAD greater than 1% of its standard deviation is a training-serving skew candidate. Sort by feature importance. A high-skew feature that ranks 47th in importance is noise; a high-skew feature that ranks 2nd is your accuracy gap. This narrows the investigation from "something is wrong" to "it is this specific feature, and here is the computation difference that causes it."
Training-serving skew from language, data source, timestamp, and preprocessing differences is not discovered through monitoring — it is prevented by building a single feature computation path that makes two diverging implementations structurally impossible. Skew from leakage needs one more discipline on top: point-in-time join enforcement.
Recap
- Same feature, wrong value: SQL exact count at train vs Redis approximate count at serve → 94% AUC drops to 82%.
- Worst on high-volume accounts — the very users most likely to be fraud.
- Five culprits, feature terms: language, data source, timestamp, preprocessing, leakage. One shared path kills four; leakage still needs point-in-time joins.
- Point-in-time join = the honesty rule: for an event at T, join only values that existed *before* T.
- Rolling window leak: feature computed an hour late can include post-event transactions → offline AUC is a mirage.
- Diagnostic: run both pipelines on same 1,000 examples, MAD per feature; MAD > 1% of std = skew candidate, sort by importance.
- "Same logic, other language" is a hope, not a fact — only true once byte-identical output is tested.
Check your understanding
Q1. You are building a loan default prediction model with a "total_outstanding_loan_balance" feature. Which two statements about implementing training and serving correctly are true?
- A) For training, use today's balance for all historical examples because it reflects the most accurate current state; for serving, also use current balance
- B) For training, retrieve balance via a temporal join (SUM where loan_opened <= label_date AND not yet closed), excluding future loans
- C) For serving, run the identical temporal-join logic at current time through a shared function, and monitor PSI weekly for drift
- D) Balance features are exempt from point-in-time requirements because the core banking ledger applies T+2 settlement, which guarantees retroactive accuracy
Q2. Your team is rebuilding a feature pipeline that has a known bug affecting 5% of users. You need to retrain the model with corrected features. What are the risks?
- A) There are no significant risks — fixing a known bug that used a fallback default of -1 for 5% of users always produces a strictly safer model regardless of operation order
- B) The main risk is that retraining takes too long — the pipeline rebuild adds roughly 3 weeks to the release calendar, and the model goes stale while it's being fixed
- C) The retrained model expects correct features, but serving may still emit buggy ones — fix serving first, then backfill, then retrain, never the reverse order
- D) The risk is that corrected features will have a materially different distribution (mean shift of roughly 12%) than what the model expects, so the only safe option is to deploy without retraining
Q3. Explain why a 30-day rolling average feature is particularly prone to training-serving skew.
- A) Rolling averages are inherently unstable due to floating-point accumulation error in Welford's online variance algorithm and should be replaced with cumulative sums
- B) The 30-day window aggregates roughly 720 hourly samples, enough to smooth out any skew — rolling averages are actually among the most skew-resistant feature types
- C) Differences in reference timestamp, timezone handling, and null treatment compound across 30 days; a centralized feature store with one authoritative pipeline is the fix
- D) Rolling averages are prone to skew only when the lookback window exceeds 7 days and crosses a daylight-saving boundary in the US-East timezone; 30-day windows are otherwise perfectly reliable
Q4. A product team wants to add a real-time "user_session_length_so_far" feature to a fraud detection model. Latency SLA is 20ms. How do you evaluate and implement this?
- A) Reject the feature immediately — any real-time feature will violate a 20ms SLA due to Redis lookup overhead, which averages 45ms P99 under the standard connection-pool config
- B) Implement it as a daily batch feature computed by a 2am Spark job and accept up to 24h staleness — real-time session features are too operationally complex for fraud systems
- C) Verify offline predictive value first; if significant, store session_start_time in Redis for sub-ms lookup and compute length at serving; replay logs to reconstruct training values
- D) Compute session length from the full event log at inference time using a 90-day Elasticsearch index with a custom scroll API — querying historical events is more accurate than maintaining Redis state
Try it interactively
ML Systems Lab is a free interview-prep platform for ML engineers — work through the full interactive module, quizzes, and drills.
Open ML Systems Lab →