Skip to main content

🐤 Canary Releases

Blue-green flips every user to the new version in one heartbeat. That's clean — but if the new version has a bug, everyone hits it at once. A canary release is more cautious: let a small slice of real users try the new version first, watch the metrics like a hawk, and only widen the rollout when the numbers say it's safe.

Week 13 · Day 2 (Tuesday: Deployment Strategies) · Lecture 2

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what a canary release is and why it minimizes the blast radius of a bad deploy
  • Contrast canary with blue-green and describe when each is the right tool
  • Configure weighted traffic splitting at a load balancer or service mesh
  • Choose the error, latency, and business metrics that decide whether to promote or roll back
  • Automate a progressive rollout that increases traffic in stages with a bake time at each step
  • Define clear rollback thresholds so a failing canary is pulled automatically

Estimated Time: 70 minutes

Practice: Configure a weighted split and write the analysis logic that promotes or rolls back the canary.

In This Lesson

What Is a Canary Release?

The name comes from coal mining: miners carried a canary underground because the bird reacted to toxic gas long before humans did. A distressed canary was an early warning to get out. In software, a canary release is a small, expendable sample of production traffic sent to the new version — if it shows distress (errors, slowness), you pull it before the whole user base is affected.

Concretely: you deploy the new version alongside the current one, then route a tiny percentage — often 1–5% — of live traffic to it. You compare the canary's metrics against the stable version. If it holds up, you increase the percentage in stages until it serves everyone. If it doesn't, you route that traffic back and the vast majority of users never noticed.

sequenceDiagram participant Users participant Router as Load Balancer participant Stable as Stable v1 participant Canary as Canary v2 Note over Router,Canary: Start with all traffic on the stable version Users->>Router: Requests Router->>Stable: Send one hundred percent of traffic Note over Canary: Deploy v2 and route five percent to it Users->>Router: Requests Router->>Stable: Send ninety five percent Router->>Canary: Send five percent Note over Router,Canary: Compare canary metrics against stable Note over Router,Canary: Metrics look healthy so raise the canary share Users->>Router: Requests Router->>Canary: Send one hundred percent Note over Stable: Retire the old version

🍽️ The restaurant special analogy

A chef with a new recipe doesn't rewrite the whole menu overnight. They offer it as a "daily special" to a handful of curious diners, gather reactions, and refine it. If it flops, only a few plates were affected — not the entire dining room. A canary release is that daily special for your code.

Canary vs Blue-Green

Both strategies run two versions and both avoid downtime, but they answer a different question. Blue-green asks "is the new environment ready to take over?" Canary asks "how is the new version behaving with real users, right now, at small scale?"

A router sending 95 percent of traffic to the stable version and 5 percent to the canary Router (weighted) Stable v1 95% of traffic 🐤 Canary v2 5% of traffic
Unlike blue-green's all-or-nothing switch, a canary router splits traffic by weight — a thick stream to stable, a thin stream to the canary.
Blue-GreenCanary
Traffic move100% at once (atomic)Gradual, by percentage
Users during rolloutAll on one versionDifferent users on different versions
Blast radius of a bugEveryone, until rollbackOnly the canary slice
Rollout speedSecondsMinutes to hours (deliberately)
Traffic-routing complexitySimple on/offWeighted routing + metric analysis
Best forAtomic cutovers, simple mental modelHigh-traffic, high-stakes services

💡 They compose

These aren't mutually exclusive. A common pattern is a canary within a broader release: shift 5% to the new version, analyze, then continue ramping. Netflix pioneered automating exactly this at scale — deploying thousands of times a day while limiting risk to a small sample first.

Splitting the Traffic

The heart of a canary is weighted routing: telling the infrastructure "send this fraction here, the rest there." Where you configure it depends on your stack.

ApproachHow it splitsTrade-off
Load balancerWeighted target groupsSimple, works anywhere; coarse control
Service mesh (Istio/Linkerd)Weighted routes + rich metricsFine-grained, request-level; more infra to run
Feature flagsPer-user targeting in app codeGreat for monoliths and specific users; needs a flag service

Load balancer: weighted target groups (AWS)

# An ALB listener that sends 90% to current and 10% to the canary.
DefaultActions:
  - Type: forward
    ForwardConfig:
      TargetGroups:
        - TargetGroupArn: !Ref CurrentTargetGroup
          Weight: 90
        - TargetGroupArn: !Ref CanaryTargetGroup
          Weight: 10
      # Keep a user pinned to one version for their session:
      TargetGroupStickinessConfig:
        Enabled: true
        DurationSeconds: 3600

Service mesh: Istio traffic splitting

# Istio VirtualService: split by weight between two subsets.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: my-app
spec:
  hosts: [ "my-app" ]
  http:
    - route:
        - destination: { host: my-app, subset: stable }
          weight: 90
        - destination: { host: my-app, subset: canary }
          weight: 10

⚠️ Pin users with session affinity

Without stickiness, the same user can bounce between versions on every request — an inconsistent, confusing experience (and it muddies your metrics). Use a consistent hash of the user or a sticky cookie so that a user who lands on the canary stays on the canary for their session.

If you're routing inside a Node service instead of at the edge, hash the user to a stable bucket so each user consistently gets one version:

// Deterministic canary routing: the same user always lands the same way.
function hashToPercent(id) {
  let h = 0;
  for (let i = 0; i < id.length; i++) {
    h = ((h << 5) - h) + id.charCodeAt(i);
    h |= 0; // force 32-bit integer
  }
  return Math.abs(h) % 100;                 // 0..99
}

function canaryMiddleware(canaryPercent) {
  return (req, res, next) => {
    const id = req.session?.userId || req.ip;
    // Below the threshold => canary; the SAME id always maps the same way.
    req.serviceVersion = hashToPercent(id) < canaryPercent ? 'canary' : 'stable';
    next();
  };
}

app.use(canaryMiddleware(10)); // send ~10% of users to the canary

Watching the Right Metrics

A canary is only as good as the signals you compare. The whole point is to catch a regression in the small sample before it reaches everyone, so you must decide in advance which numbers matter and what "worse" means.

graph TD A["Canary metrics"] --> B["Error rate
5xx responses, exceptions"] A --> C["Latency
p50, p95, p99 response time"] A --> D["Throughput
requests handled per second"] A --> E["Business signals
checkout completion, conversions"]

The strongest technique is a relative comparison: canary metric divided by stable metric over the same window. A ratio near 1.0 means the canary behaves like the baseline; a ratio well above 1.0 for errors or latency is your signal to roll back.

# Prometheus recording rules comparing canary to baseline (a ratio).
groups:
  - name: canary_analysis
    rules:
      # Error-rate ratio: canary errors vs baseline errors.
      - record: canary:error_ratio
        expr: |
          (sum(rate(http_requests_total{version="canary",status=~"5.."}[5m]))
             / sum(rate(http_requests_total{version="canary"}[5m])))
          /
          (sum(rate(http_requests_total{version="stable",status=~"5.."}[5m]))
             / sum(rate(http_requests_total{version="stable"}[5m])))

      # Latency ratio: average canary latency vs baseline.
      - record: canary:latency_ratio
        expr: |
          (sum(rate(http_request_duration_seconds_sum{version="canary"}[5m]))
             / sum(rate(http_request_duration_seconds_count{version="canary"}[5m])))
          /
          (sum(rate(http_request_duration_seconds_sum{version="stable"}[5m]))
             / sum(rate(http_request_duration_seconds_count{version="stable"}[5m])))

Reading the ratios

canary:error_ratio   → 1.02   // ~2% more errors: within tolerance
canary:latency_ratio → 1.35   // 35% slower: exceeds a 15% threshold → roll back

✅ Give the canary enough traffic to be meaningful

1% of a firehose is plenty of signal; 1% of a trickle is statistical noise. Pick a starting percentage that yields enough requests to make the comparison trustworthy, and give each stage a "bake time" (often 10–30 minutes) so slow-burning issues like memory leaks have a chance to appear.

Progressive Rollout

You rarely jump from 5% to 100%. Instead you climb a staircase, pausing at each step to analyze. If any step fails its thresholds, you abandon the climb and route everything back to stable.

graph TD A["Start: 100% stable"] --> B["Step 1: 5% canary"] B --> C["Analyze metrics"] C -->|"Pass"| D["Step 2: 25% canary"] C -->|"Fail"| Z["Roll back to 100% stable"] D --> E["Analyze metrics"] E -->|"Pass"| F["Step 3: 50% canary"] E -->|"Fail"| Z F --> G["Analyze metrics"] G -->|"Pass"| H["Promote: 100% canary"] G -->|"Fail"| Z

A minimal analysis loop makes the promote-or-rollback decision explicit. Read the ratios, compare to thresholds, and act:

#!/usr/bin/env bash
set -euo pipefail

PROM="http://prometheus:9090"
ERROR_MAX=1.10    # allow at most 10% more errors than baseline
LATENCY_MAX=1.15  # allow at most 15% more latency than baseline
BAKE=600          # analyze each stage for 10 minutes
INTERVAL=30

query() { curl -s -G "$PROM/api/v1/query" \
  --data-urlencode "query=$1" | jq -r '.data.result[0].value[1] // "1"'; }

deadline=$(( $(date +%s) + BAKE ))
while [ "$(date +%s)" -lt "$deadline" ]; do
  error_ratio=$(query 'canary:error_ratio')
  latency_ratio=$(query 'canary:latency_ratio')
  echo "error_ratio=$error_ratio latency_ratio=$latency_ratio"

  if (( $(echo "$error_ratio > $ERROR_MAX" | bc -l) )); then
    echo "Error ratio too high — rolling back the canary."; exit 1
  fi
  if (( $(echo "$latency_ratio > $LATENCY_MAX" | bc -l) )); then
    echo "Latency ratio too high — rolling back the canary."; exit 1
  fi
  sleep "$INTERVAL"
done

echo "Stage healthy — promote to the next traffic weight."

⚠️ Databases still need backward compatibility

Because stable and canary run side by side for longer than blue-green, both versions read and write the same database at the same time. The expand-contract pattern from the blue-green lesson is even more important here: additive changes first, dual-write during the ramp, drop old columns only after the canary is fully promoted.

Automating with Flagger

Hand-rolling the loop above teaches the mechanics, but in production you'll lean on a tool that does the ramp-and-analyze cycle for you. On Kubernetes, Flagger (paired with a service mesh) and Argo Rollouts are the popular choices. You declare the steps and metric thresholds; the operator drives the rollout and rolls back automatically on failure.

# Flagger Canary: raise traffic 10% at a time up to 50%,
# checking metrics every minute and rolling back after 5 failures.
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: my-app
  namespace: prod
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  provider: istio
  service:
    port: 80
    targetPort: 8080
  analysis:
    interval: 1m        # analyze once per minute
    threshold: 5        # roll back after 5 failed checks
    maxWeight: 50       # ramp the canary up to 50% before promoting
    stepWeight: 10      # increase by 10 percentage points each step
    metrics:
      - name: request-success-rate
        thresholdRange: { min: 99 }   # require >= 99% success
        interval: 1m
      - name: request-duration
        thresholdRange: { max: 500 }  # p99 latency under 500ms
        interval: 1m

The same idea in Argo Rollouts, where you spell out the traffic weights and pause durations directly:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
spec:
  replicas: 5
  selector:
    matchLabels: { app: my-app }
  template:
    metadata:
      labels: { app: my-app }
    spec:
      containers:
        - name: my-app
          image: my-app:v2
          ports: [ { containerPort: 8080 } ]
  strategy:
    canary:
      steps:
        - setWeight: 5
        - pause: { duration: 10m }   # bake at 5%
        - setWeight: 25
        - pause: { duration: 10m }   # bake at 25%
        - setWeight: 50
        - pause: { duration: 10m }   # bake at 50%
        # then Argo promotes to 100% automatically

💡 Let the robot pull the trigger

Humans are slow and inconsistent at 3 a.m. Encoding your thresholds into the tool means a bad canary is caught and reverted in seconds, every time, without waking anyone. Your job shifts from watching dashboards to choosing good thresholds.

Practice & Quiz

🏋️ Exercise 1: Pick the rollback thresholds

Goal: For a checkout service, decide which metrics gate the canary and what ratio should trigger an automatic rollback. Which metric deserves near-zero tolerance?

💡 Hint

Money-losing failures deserve the strictest thresholds. Compare canary to baseline as ratios and think about what a 1% drop in one number actually costs.

✅ Solution
  • Payment error rate: near-zero tolerance — any increase over baseline triggers rollback.
  • Checkout completion rate: roll back if it drops more than ~1% vs stable.
  • p95 latency: roll back above ~1.15× baseline.
  • Server 5xx rate: roll back above ~1.10× baseline. Run each stage for at least 10–30 minutes.

🏋️ Exercise 2: Fix the flapping user experience

Goal: Users report the site "keeps changing" during your canary — a new layout appears and disappears on refresh. What is wrong, and how do you fix it?

✅ Solution

The router lacks session affinity, so each request is independently bucketed and a user bounces between stable and canary. Fix it with sticky sessions (a cookie) or a deterministic hash of the user id, so a given user consistently gets one version for their whole session.

🎯 Quick Quiz

Question 1: What is the defining behavior of a canary release?

Question 2: Why compare the canary's metrics as a ratio against the stable version rather than an absolute number?

Question 3: Why is a "bake time" at each traffic stage important?

Best Practices & Pitfalls

✅ Do

  • Start small (1–5%) and increase in deliberate stages
  • Compare canary vs stable as ratios over the same time window
  • Define automatic rollback thresholds before you deploy
  • Use session affinity so a user stays on one version
  • Give each stage a bake time long enough to reveal slow-burning bugs
  • Keep database changes backward-compatible (expand-contract, dual-write)
  • Include a business metric — technical health can look fine while conversions tank

❌ Don't

  • Don't jump straight to a large percentage — that defeats the purpose
  • Don't rely on a human staring at dashboards; automate the decision
  • Don't start a canary so small that the metrics are just noise
  • Don't make breaking schema changes while both versions run side by side
  • Don't forget to eventually decommission the old version after full promotion

✅ Canary shines for high-traffic, high-stakes services

The extra complexity of weighted routing and metric analysis pays off most when a bug would be expensive and when you have enough traffic to get a fast, trustworthy read from a small sample. For a low-traffic internal tool, a simpler rolling update is often the better fit.

Summary

🎉 Key Takeaways

  • A canary release routes a small percentage of real traffic to the new version as an early warning system
  • Its big win over blue-green is a tiny blast radius — a bug hits only the canary slice
  • Weighted routing (load balancer or service mesh) splits the traffic; session affinity keeps users consistent
  • Decide promote-or-rollback by comparing canary and stable metrics as ratios against fixed thresholds
  • Roll out progressively with a bake time at each stage, and automate the whole cycle with Flagger or Argo Rollouts
  • Keep database changes backward-compatible, because both versions run side by side longer than in blue-green

📚 Additional Resources

🚀 What's Next?

Both blue-green and canary run a separate copy of the new version. But the most common day-to-day strategy simply upgrades your existing instances a few at a time — no second environment, minimal extra capacity. That's the Rolling Update, and it's next.

🎉 Nicely done!

You can now roll out a change to a cautious sliver of users and let the metrics decide its fate. Next, the workhorse strategy every orchestrator gives you for free.