Model Monitoring & Drift Detection in Production

Deploying a model to production is not the finish line — it is the starting gun for a continuous monitoring challenge. Unlike traditional software, ML models can degrade silently: no exceptions are thrown, no alerts fire, but the model is quietly making worse predictions because the world has changed.

This article covers the full monitoring stack for production ML systems: what to measure, how to detect drift, how to set up alerting, and when to trigger automated retraining.

What Can Go Wrong in Production?

ML models fail in production for three distinct reasons:

Lesson learned: In practice, infrastructure failures are the most common cause of model degradation and the hardest to detect if you're only monitoring model output metrics. Always monitor data quality at the pipeline entry point.

The Monitoring Stack

A complete monitoring setup has four layers:

Layer 1
Infrastructure
Latency, throughput, memory, CPU, error rates
Layer 2
Data Quality
Null rates, out-of-range values, schema violations
Layer 3
Feature Drift
Distribution shift vs training baseline
Layer 4
Model Performance
Accuracy, AUC, precision, business KPIs

Data Quality Monitoring

Before checking if the model is drifting, check if the data pipeline is healthy. Great Expectations and Pydantic are excellent tools for defining and enforcing data quality contracts.

from great_expectations.core import ExpectationSuite import great_expectations as gx context = gx.get_context() # Define expectations on the incoming feature dataset suite = context.add_expectation_suite("features.production") # Validate expected schema and value ranges validator.expect_column_values_to_not_be_null("customer_age") validator.expect_column_values_to_be_between("transaction_amount", min_value=0, max_value=100000) validator.expect_column_values_to_be_in_set("country_code", ["BR", "US", "DE", "FR"]) validator.expect_column_mean_to_be_between("credit_score", min_value=500, max_value=750) # Run validation results = validator.validate() if not results.success: # Alert and halt pipeline raise DataQualityException(f"Validation failed: {results.statistics}")

Detecting Feature Drift with Statistical Tests

Feature drift detection compares the current serving distribution against the training baseline using statistical tests. The most commonly used are:

from evidently.report import Report from evidently.metric_preset import DataDriftPreset, DataQualityPreset import pandas as pd # reference_data = training dataset or recent stable window # current_data = last 24h of production data report = Report(metrics=[ DataDriftPreset(drift_share=0.3), # alert if >30% features drift DataQualityPreset(), ]) report.run(reference_data=reference_df, current_data=current_df) report.save_html("drift_report.html") # Extract drift summary result = report.as_dict() drifted_features = [ feat for feat, info in result["metrics"][0]["result"]["drift_by_columns"].items() if info["drift_detected"] ] if len(drifted_features) / total_features > 0.3: send_alert(f"Drift detected in {len(drifted_features)} features: {drifted_features}")

Model Performance Monitoring

When Labels Are Available (Low Latency)

For models where ground truth labels arrive quickly — fraud detection (chargeback within hours), click prediction (feedback within seconds) — you can compute real-time performance metrics and compare against the champion model's baseline.

# Prometheus metrics for model performance (Python) from prometheus_client import Gauge, Counter, Histogram PREDICTION_LATENCY = Histogram( "model_prediction_duration_seconds", "Time to generate a single prediction", buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5] ) MODEL_AUC = Gauge("model_auc_score", "Rolling 24h AUC", ["model_version"]) DRIFT_SCORE = Gauge("feature_drift_psi", "PSI score per feature", ["feature_name"]) PREDICTION_ERRORS = Counter("model_prediction_errors_total", "Total prediction errors") # In the serving endpoint @PREDICTION_LATENCY.time() def predict(features): return model.predict(features)

When Labels Are Delayed (High Latency)

For models where ground truth is delayed — loan default (months), churn (quarterly) — you cannot wait for labels to know if the model is degrading. Instead, use proxy metrics:

Alerting and Thresholds

Not all drift requires immediate action. Define tiered alert levels:

# Alerting thresholds (Prometheus alerting rules) groups: - name: model_monitoring rules: - alert: FeatureDriftWarning expr: feature_drift_psi{feature_name!=""} > 0.1 for: 1h labels: severity: warning annotations: summary: "Feature {{ $labels.feature_name }} PSI={{ $value | humanize }}" - alert: FeatureDriftCritical expr: feature_drift_psi{feature_name!=""} > 0.25 for: 30m labels: severity: critical annotations: summary: "Critical drift — retraining required" - alert: ModelAUCDegraded expr: model_auc_score < 0.78 for: 2h labels: severity: critical annotations: summary: "Model AUC {{ $value | humanize }} below threshold 0.78"

Automated Retraining Triggers

Manual retraining is reactive and slow. Build retraining triggers into the monitoring pipeline:

  1. Schedule-based: Retrain weekly or monthly. Simple, predictable, but may retrain when unnecessary or fail to retrain fast enough.
  2. Performance-based: Trigger retraining when AUC drops below threshold. Requires labeled feedback loop.
  3. Drift-based: Trigger retraining when PSI exceeds threshold for a sustained period. Works without labels — suitable for high-latency feedback models.

Best practice: Combine all three triggers with an AND logic for high-stakes models: retrain only when drift is detected AND it has persisted for more than 48 hours AND the weekly schedule is due. This prevents unnecessary retraining on transient anomalies.

The Monitoring Dashboard

A Grafana dashboard for ML models should surface at a glance:

Key Takeaways

Back to all articles