Deploy Models Without Deploying Risk
Rolling out a new ML model to 100% of production traffic on day one is a bet that the model works as well in production as it did in evaluation. That bet fails more often than ML teams want to admit. Canary deployments let you test that bet with 1-5% of traffic before committing, turning a potential disaster into a controlled experiment.
I've managed canary rollouts for recommendation models, fraud detection systems, and pricing engines. The technical setup is straightforward — traffic splitting through a load balancer or service mesh — but the operational details determine whether canaries actually catch problems or just add deployment latency without value.
Traffic Splitting Architecture
There are two ways to split traffic for model canaries: at the routing layer (load balancer or service mesh) and at the application layer (the serving application itself decides which model to call).
# Istio VirtualService for model canary
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: fraud-model-service
spec:
hosts:
- fraud-model.ml.svc.cluster.local
http:
- route:
- destination:
host: fraud-model-stable.ml.svc.cluster.local
weight: 95
- destination:
host: fraud-model-canary.ml.svc.cluster.local
weight: 5
Routing-layer splitting is cleaner architecturally but coarser. The load balancer doesn't know anything about your model — it just routes percentages. Application-layer splitting gives you more control: you can route specific user segments to the canary, ensure the same user always sees the same model version (session stickiness), and log which version served each prediction for analysis.
We use application-layer splitting for most ML canaries. The serving application hashes the user ID to determine canary assignment, which gives consistent assignment and the ability to target specific segments.
Monitoring the Canary
A canary without monitoring is just a slow rollout. You need real-time comparison between the canary model and the stable model across the metrics that matter.
See also: Camera Calibration and Geometric Transforms for 3D Vision Ap.
from dataclasses import dataclass
@dataclass
class CanaryMetrics:
model_version: str
request_count: int
latency_p50_ms: float
latency_p99_ms: float
error_rate: float
prediction_mean: float
prediction_std: float
def compare_canary(stable: CanaryMetrics, canary: CanaryMetrics) -> dict:
checks = {}
latency_ratio = canary.latency_p99_ms / stable.latency_p99_ms
checks["latency"] = {
"status": "pass" if latency_ratio < 1.15 else "fail",
"stable_p99": stable.latency_p99_ms,
"canary_p99": canary.latency_p99_ms,
"ratio": round(latency_ratio, 3),
}
checks["errors"] = {
"status": "pass" if canary.error_rate <= stable.error_rate * 1.5 else "fail",
"stable_rate": stable.error_rate,
"canary_rate": canary.error_rate,
}
pred_diff = abs(canary.prediction_mean - stable.prediction_mean)
checks["prediction_distribution"] = {
"status": "pass" if pred_diff < 0.05 else "warn",
"mean_difference": round(pred_diff, 4),
}
return checks
Graduated Rollout Strategy
Don't jump from 5% to 100%. A graduated rollout — 5% for 4 hours, 25% for 12 hours, 50% for 24 hours, 100% — gives you multiple checkpoints where automated or manual review can catch problems. Each step increases the blast radius, so the monitoring thresholds should tighten at each stage.
At 5%, a prediction distribution shift might not be statistically significant with limited data. By 25%, you have enough data to detect meaningful differences. By 50%, you should have high confidence that the canary is performing at least as well as the stable version.
Rollback Mechanics
Rollback needs to be fast and automatic. When the canary monitoring detects a problem, traffic should shift back to 100% stable within seconds, not minutes. This means the stable model's infrastructure must maintain full capacity throughout the canary period — don't scale down the stable instances just because the canary is handling some traffic.
Our rollback is triggered by any of: error rate exceeding 2x the stable model's rate, p99 latency exceeding 1.5x the stable model's latency, prediction distribution divergence exceeding a threshold calibrated per model, or a manual trigger from the on-call engineer.
After a rollback, the canary deployment is locked. The team has to investigate the failure, write a postmortem, and get explicit approval before the next canary attempt. This prevents the "just try again, maybe it was a fluke" pattern that leads to the same model failing repeatedly.
Related reading: Prompt Engineering as Software Engineering: Version Control .
Session Stickiness for User-Facing Models
For user-facing models (recommendations, search ranking, content personalization), session stickiness is critical. A user who gets recommendations from the canary model on one page load and from the stable model on the next will have an inconsistent experience. Worse, you can't properly attribute user behavior to a specific model version without consistent assignment.
Hash-based assignment using the user ID ensures consistency. The user always sees the same model version throughout the canary period. When the canary promotes to stable, the transition is invisible because the output is (presumably) similar enough that users don't notice the shift.
Canary Duration and Decision Criteria
How long should a canary run? Long enough to observe the metrics you care about, but not so long that you're perpetually in a half-deployed state.
For real-time models (fraud detection, recommendations), 24-48 hours at 5% traffic usually provides enough data for statistical comparison. For batch models (weekly retraining), you need at least one full batch cycle to compare performance. For models with delayed feedback (ad click models where conversions happen days later), the canary period needs to be long enough for feedback to arrive.
The decision criteria should be defined before the canary starts, not after. "We'll promote if the metrics look good" is too vague. "We'll promote if: (a) the canary's primary metric is within 2% of the stable model with 95% confidence, (b) all guardrail metrics are within thresholds, and (c) no anomalies are detected in the prediction distribution" is specific enough to automate.
Related reading: Building Production Tokenizers: BPE, WordPiece, and Sentence.
Automating the promotion decision removes the temptation to promote a borderline canary because the team is eager to ship. If the canary doesn't meet the pre-defined criteria, it doesn't promote. That's not a failure — it's the system working as intended.
Multi-Model Canary Coordination
Real systems often have multiple models in the serving path. A search system might use a retrieval model, a ranking model, and a diversity model in sequence. When you update one of these models, the canary needs to test the updated model in context with the production versions of the others.
We handle this with "model stacks" — versioned combinations of all models in a serving path. The canary deploys a new stack where one model is updated and the rest are pinned to production versions. This isolates the effect of the single model change, which is what you want for clean measurement.
# Model stack definition for canary
model_stack_stable = {
"retrieval": "retrieval-v23",
"ranking": "ranking-v45",
"diversity": "diversity-v12",
}
model_stack_canary = {
"retrieval": "retrieval-v23",
"ranking": "ranking-v46", # new version being tested
"diversity": "diversity-v12",
}
# Traffic routing based on stack, not individual model
def route_request(user_id: str) -> dict:
if hash(user_id) % 100 < 5: # 5% canary
return model_stack_canary
return model_stack_stable
The complication arises when you want to update multiple models simultaneously. Each model should ideally be canary-tested independently before combining them, because interaction effects between model updates can mask or amplify individual model impacts. We enforce a rule that no more than one model in a stack can be in canary at the same time.
Feature Flag Integration
Model canaries work well with feature flag systems. Instead of routing traffic at the load balancer level, you use your existing feature flag infrastructure (LaunchDarkly, Unleash, or whatever your organization uses) to control model version assignment. This gives you all the targeting, gradual rollout, and emergency kill-switch capabilities that feature flag systems already provide.
The integration is straightforward: the serving application checks a feature flag to determine which model version to use for each request. The feature flag system handles percentage rollouts, user targeting, and emergency shutoffs. Your ML monitoring system watches the metrics and triggers the shutoff through the feature flag API if the canary fails.
This approach also makes it easy to run targeted canaries — testing the new model only on a specific geographic region, or only on users in a particular segment. Targeted canaries are useful when you expect the model change to affect specific populations differently, and you want to validate on the most impacted group before rolling out broadly.