{"title":"Use Canary Analysis as a Production Test","excerpt":"A 5% canary failing 0.49% of its requests showed up as 0.025% on the service dashboard, a burn rate of 0.25 that no SLO alert would page on. This guide builds canary analysis as a controlled experiment: a fresh baseline in the same window, stages priced in error budget, and deterministic stop rules with sample floors, tested against four releases in a lab driven by k6 v2.3.0. It then maps the same comparison onto Argo Rollouts v1.10.1 experiments, Flagger and Kayenta, and adds a k6 probe for paths real traffic rarely reaches.","canonicalUrl":"https://automationtester.in/blog/automation-tutorials/canary-analysis-production-testing-slos","category":{"name":"Automation Tutorials","slug":"automation-tutorials"},"tags":["canary-testing","progressive-delivery","slo","argo-rollouts","k6","observability"],"author":{"name":"Shashank Rawlani","url":"https://shashank.rawlani.com","linkedin":"https://www.linkedin.com/in/shashankrawlani/"},"datePublished":"2026-10-26T13:30:00.000Z","dateModified":"2026-10-26T13:30:00.000Z","coverImage":{"url":"https://ik.imagekit.io/automationtester/automationtester-in/blog/covers/v3/canary-analysis-production-testing-slos.webp","alt":"A wide green band of traffic dots runs across the top of the frame. On the left a ringed node sheds two equal thin lanes that run side by side, joined by short vertical ticks. Two gates of posts cross both lanes and the lanes thicken after the first. Between the first and second gates the lower lane turns orange and carries large orange dots while the upper lane stays green; at the second gate the lower lane ends at an orange crossed-out circle, the upper lane ends at a small green node, and a green curve carries the lower lane's traffic back up into the wide band. Beyond, both lanes continue only as thick grey dashes through a faded third gate. Along the bottom sit a short green pill, a longer orange pill and a long dashed outline."},"contentHtml":"<div class=\"callout callout-info\"><strong>Quick answer:</strong> Treat a canary as an experiment with a control group. Send equal slices of traffic to the new version and to a freshly started baseline of the old version, and compare the two over the same time window, never against last hour's numbers. Raise exposure in stages, and price each stage in error budget: the traffic share multiplied by the time it takes your rules to stop it. Small stages judge only blunt signals such as hard failures; error and latency deltas wait for a sample floor in both cohorts, and resource use comes last. Write the stop rules as code, keep an absolute SLO check on the control as well, and record every promote, pause and abort with the numbers behind it.</div>\n\n<p>A release goes to 5% of traffic. The service dashboard shows an error ratio of 0.025%, a quarter of what a 99.9% objective allows, so the rollout continues. Every customer who applies a coupon at checkout on the new version gets a 500. Over the same minute, the new version's own error ratio was 0.49%, about five times the objective's allowance. Both numbers come from the same 24,003 requests in the lab behind this article: the service view divided 6 failures by all of them, the canary view by the 1,216 the new version served.</p>\n\n<p>The earlier parts of this series tested before production, from <a href=\"/blog/automation-tutorials/k6-thresholds-fail-ci-right-reasons\">thresholds that fail CI for the right reasons</a> to capacity. Google's SRE workbook is plain about where that stops: test environments differ from production, and some defects only appear under real traffic. Canary testing needs the same discipline as a k6 gate: an objective, a control, enough samples and a rule that fails when it should.</p>\n\n<div class=\"callout callout-warning\"><strong>Versions checked on 2026-10-19:</strong> Argo Rollouts v1.10.1 (released 2026-10-12), Flagger v1.45.0 (2026-09-01), Kayenta as shipped in Spinnaker release 2026.1.3 (2026-09-25; the standalone Kayenta repository is archived and the code now lives in the Spinnaker monorepo), and k6 v2.3.0. The lab is a Node.js 26.0.0 service on macOS arm64 that plays three pod groups behind a weighted router, driven at 400 requests per second by k6 v2.3.0. Its stages last 60 seconds where production stages last tens of minutes. The Argo Rollouts YAML below was not run, because the lab has no cluster.</div>\n\n<h2 id=\"cnry-dilution-math\">Why the service view cannot see a small canary</h2>\n\n<p>The dilution is plain arithmetic. A canary serving a share <em>w</em> of traffic with error ratio <em>e</em> adds <em>w × e</em> to the service's error ratio. The SRE workbook's own example: a release failing 20% of requests, canaried to 5%, shows up as a 1% service-wide error rate. The chapter then notes how hard that is to see against the overall trend.</p>\n\n<p>The same arithmetic tells you that your normal SLO alerts are blind during a canary. The workbook's recommended fast-burn page fires at a burn rate of 14.4, which for a 99.9% objective is a 1.44% error ratio. A 1% canary that fails every request moves the service error ratio by 1%, a burn rate of 10, so on its own it can never trigger that page. At 5%, the canary would have to fail 28.8% of its requests (0.0144 ÷ 0.05) before the page fired. In the coupon run, the canary's 0.49% added up to a service-wide burn rate of 0.25.</p>\n\n<p>Here is stage 2 of that run, with canary and baseline at 5% each, split by the version that served each request:</p>\n\n<pre class=\"language-text\"><code class=\"language-text\">window 92-152s\n  all      n= 24003  5xx=   6 (0.02%)  order_p95=43.8ms  cpu/req=0.216ms\n  stable   n= 21604  5xx=   0 (0.00%)  order_p95=43.8ms  cpu/req=0.215ms\n  baseline n=  1183  5xx=   0 (0.00%)  order_p95=43.8ms  cpu/req=0.216ms\n  canary   n=  1216  5xx=   6 (0.49%)  order_p95=43.9ms  cpu/req=0.217ms</code></pre>\n\n<p>Latency and CPU did not move, because the defect only touched one rare path: 20% of requests are orders and 3% of orders carry a coupon, so it hit about 0.6% of the canary's traffic. Whatever you compare, it has to be split by version. If the service already exports OpenTelemetry, the <code>service.version</code> resource attribute is the standard place for that label, and <a href=\"/blog/automation-tutorials/connect-load-tests-opentelemetry-traces\">part 5 on connecting load tests to traces</a> covers carrying test context into traces and logs without exploding its cardinality.</p>\n\n<h2 id=\"cnry-choose-the-control\">Choose the control before you choose the metrics</h2>\n\n<p>A canary verdict is a comparison, and it is only as good as what the canary is compared with.</p>\n\n<p><strong>Before and after.</strong> Compare the service after the deploy with the service before it. The workbook warns against it, because time is one of the biggest sources of change in observed metrics. To show why, the lab added 25 ms to every version, old and new, from second 31, the moment the rollout began, standing in for a batch job, a traffic shift or a slower dependency. Order p95 across the service went from 44.0 ms before the rollout to 69.0 ms after it. A before-and-after gate at 1.25× would have rolled back a release with nothing wrong in it. Over stage 2, canary against baseline read 69.3 ms against 68.9 ms, and the gate promoted all three stages.</p>\n\n<p><strong>Against the stable fleet.</strong> This is better, because both sides share the clock. It still compares pods that started minutes ago with pods that have run for days. Spinnaker's canary best-practices page asks for a baseline deployed at the same time, at the same size and with the same traffic, and names cache warm-up time and heap size among the factors this removes.</p>\n\n<p><strong>Against a fresh baseline.</strong> Start new pods of the current version alongside the canary, and give both the same traffic weight. Argo Rollouts builds this in as an Experiment step, and the lab's gate does the same on every stage.</p>\n\n<p>The window matters as much as the cohort. In another run, the stable fleet threw 503s on 2.70% of requests for the first 25 seconds, before any canary existed, and the coupon defect then went out as the canary. Over stage 2 the canary's error ratio was 0.79% and the baseline's was 0.00%, a delta of 0.79 points. Compared with the stable fleet over a lookback that began at second 0, the control read 0.48% and the delta fell to 0.31 points. The incident had nothing to do with the release, yet it hid 61% of the signal. That is the workbook's rule: metric intervals the same as or shorter than the canary.</p>\n\n<h2 id=\"cnry-staged-exposure\">Stage the exposure and price each stage in budget</h2>\n\n<p>The workbook's model of a canary's cost is the traffic share multiplied by the time a defect is exposed, and it charges that against the error budget. For a 99.9% objective over 30 days, the budget is 0.1% of requests: the equivalent of 43.2 minutes of total outage. This script prices three typical stages:</p>\n\n<pre class=\"language-javascript\"><code class=\"language-javascript\">// budget.mjs: share of a 30-day error budget a canary stage can spend.\nconst SLO = 0.999;\nconst PERIOD_MIN = 30 * 24 * 60;              // 43,200 minutes\nconst budgetMin = (1 - SLO) * PERIOD_MIN;      // 43.2 minutes of total outage\n\nfunction stageCost({ weight, minutes, canaryErrorRate }) {\n  const failedMinutes = (weight / 100) * canaryErrorRate * minutes;\n  return (failedMinutes / budgetMin) * 100;   // percent of the 30-day budget\n}</code></pre>\n\n<pre class=\"language-text\"><code class=\"language-text\">budget: 43.2 minutes of full outage per 30 days\n1%  for 30 min: all requests fail, whole stage 0.69% | stopped after 10 min 0.23% | 0.6% failing, whole stage 0.004%\n5%  for 30 min: all requests fail, whole stage 3.47% | stopped after 10 min 1.16% | 0.6% failing, whole stage 0.021%\n25% for 60 min: all requests fail, whole stage 34.72% | stopped after 10 min 5.79% | 0.6% failing, whole stage 0.208%</code></pre>\n\n<p>Two things follow. First, a stage's real cost is the share multiplied by the time to stop, not by the stage's length. An hour at 25% costs a third of the month's budget only if nothing stops it, so the detection loop matters more than the stage plan. Second, the coupon defect costs almost nothing at any stage; the rules exist to keep a badly broken release away from 25%.</p>\n\n<p>Stages also decide what you can measure. The workbook recommends judging the first, smallest stage on the clearest signals, such as crashes and request failures, and adding metrics that need more data as the population grows. In the lab, stage 1 at 1% gave the canary 228 requests per minute, too few to tell 0.49% from zero. The lab's stage plan follows that order:</p>\n\n<pre class=\"language-javascript\"><code class=\"language-javascript\">// canary-gate.mjs (excerpt): stages, and two of the five rules\nconst SLO = 0.999;            // 99.9% of requests succeed, 30-day window\nconst BUDGET = 1 - SLO;       // 0.1% of requests may fail\n\nconst STAGES = [\n  { weight: 1,  holdS: 60, rules: ['hard', 'control'] },\n  { weight: 5,  holdS: 60, rules: ['hard', 'control', 'errors', 'latency'] },\n  { weight: 25, holdS: 60, rules: ['hard', 'control', 'errors', 'latency', 'cpu'] },\n];\n\nconst RULES = {\n  // ...hard: canary 5xx ratio above 20x budget with at least 3 failures\n  errors: ({ canary, baseline }) =&gt; {\n    if (canary.n &lt; 1000 || baseline.n &lt; 1000) return { verdict: 'wait', why: `need 1000 requests per cohort, have ${canary.n}/${baseline.n}` };\n    const delta = canary.errors / canary.n - baseline.errors / baseline.n;\n    return delta &gt; 2 * BUDGET\n      ? { verdict: 'fail', why: `error delta ${(delta * 100).toFixed(2)} pts &gt; ${(2 * BUDGET * 100).toFixed(1)} pts` }\n      : { verdict: 'pass', why: `error delta ${(delta * 100).toFixed(2)} pts` };\n  },\n  control: ({ baseline, stable }) =&gt; {\n    for (const [name, g] of Object.entries({ baseline, stable })) {\n      const ratio = g.errors / g.n;\n      if (ratio &gt; 14.4 * BUDGET) return { verdict: 'fail', why: `${name} 5xx ${(ratio * 100).toFixed(2)}% &gt; ${(14.4 * BUDGET * 100).toFixed(2)}% (14.4x burn) during canary` };\n    }\n    return { verdict: 'pass', why: 'control inside 14.4x burn' };\n  },\n  // ...latency: order p95 ratio above 1.25 with 200 orders per cohort; cpu: CPU per request ratio above 1.3\n};</code></pre>\n\n<p>Every 10 seconds the gate re-reads the request log, evaluates the stage's rules over the canary and baseline cohorts in the same window, and aborts on the first failure. A rule that still lacks its sample floor at the end of a stage returns <code>wait</code>, and the stage pauses for a human instead of promoting. Three releases went through it:</p>\n\n<table>\n<thead><tr><th>Release</th><th>Stage 1 (1%)</th><th>Stage 2 (5%)</th><th>Stage 3 (25%)</th></tr></thead>\n<tbody>\n<tr><td>Coupon orders return 500</td><td>Promoted, 0 of 228 failed</td><td>Aborted: error delta 0.49 points, 6 of 1,216 failed</td><td>Never reached</td></tr>\n<tr><td>Extra CPU work on every request</td><td>Promoted</td><td>Promoted, order p95 ×1.02</td><td>Aborted: 1.249 against 0.216 ms per request (×5.77)</td></tr>\n<tr><td>No defect</td><td>Promoted</td><td>Promoted, delta 0.00 points</td><td>Promoted, CPU ×0.99</td></tr>\n</tbody>\n</table>\n\n<p>The CPU regression did not hurt users, since latency stayed within 2%, which is why the workbook, for its example service, prefers return codes and latency to CPU usage. It still matters at full rollout: an extra millisecond on each of 400 requests a second is about 0.41 of a core, the headroom <a href=\"/blog/automation-tutorials/capacity-testing-bottlenecks-queues-pools\">capacity testing</a> exists to measure. Measure CPU spent serving the request, as the lab does, not host CPU, which the workbook notes also moves with other processes such as log rotation. One gap in the lab's rule: <code>cpu</code> has no sample floor, so it aborted after 958 canary requests. Give it one before relying on it.</p>\n\n<p>Each decision is written down as it is made:</p>\n\n<pre class=\"language-json\"><code class=\"language-json\">{\"at_s\":92,\"from_ms\":32002,\"to_ms\":92012,\"stage\":1,\"weight\":1,\"decision\":\"promote\",\"reasons\":[\"hard pass: canary 5xx 0/228\",\"control pass: control inside 14.4x burn\"]}\n{\"at_s\":152,\"from_ms\":92012,\"to_ms\":152029,\"stage\":2,\"weight\":5,\"decision\":\"abort\",\"reasons\":[\"hard pass: canary 5xx 6/1216\",\"control pass: control inside 14.4x burn\",\"errors fail: error delta 0.49 pts &gt; 0.2 pts\",\"latency pass: order p95 x1.00\"]}</code></pre>\n\n<p>That file is the evidence a release review needs: the window compared, the rule that fired, and on how many requests. An AI assistant can summarise these records into a readable account of what changed. It should not decide to promote; the rules, thresholds and rollback policy are code a human reviewed before the rollout began.</p>\n\n<h2 id=\"cnry-shared-mutable-state\">When the canary breaks the control</h2>\n\n<p>The workbook warns that canary and control may share backends, data stores and other infrastructure, so a bad canary can damage the control and confuse the comparison. The lab reproduced that with one more release. Stage 1 is the first window below, stage 2 the second. The new version wrote product entries to a shared cache in a new shape. It could read both shapes, but the old version could not read the new one:</p>\n\n<pre class=\"language-text\"><code class=\"language-text\">window 32-92s\n  all      n= 24002  5xx= 142 (0.59%)  order_p95=43.9ms  cpu/req=0.219ms\n  stable   n= 23511  5xx= 141 (0.60%)  order_p95=43.9ms  cpu/req=0.219ms\n  canary   n=   242  5xx=   0 (0.00%)  order_p95=43.0ms  cpu/req=0.220ms\n  baseline n=   249  5xx=   1 (0.40%)  order_p95=41.3ms  cpu/req=0.218ms\nwindow 92-102s\n  all      n=  4001  5xx=  90 (2.25%)  order_p95=44.0ms  cpu/req=0.221ms\n  stable   n=  3581  5xx=  86 (2.40%)  order_p95=44.0ms  cpu/req=0.221ms\n  baseline n=   212  5xx=   4 (1.89%)  order_p95=44.8ms  cpu/req=0.219ms\n  canary   n=   208  5xx=   0 (0.00%)  order_p95=44.0ms  cpu/req=0.224ms</code></pre>\n\n<p>All 260 failures in the run were served by the old version. A comparison-only gate reads this as a canary healthier than its control. The 1% canary was also costing more than its own traffic share: 0.59% of all requests failed in stage 1. The gate's <code>control</code> rule, an absolute SLO check on the baseline and the stable fleet, aborted the rollout ten seconds into stage 2. It did not fire in stage 1, though, because 0.60% sits below the 1.44% fast-burn line. A lower threshold, such as 5× (0.5%), would have stopped it by the end of stage 1. The workbook's advice is to keep absolute measures such as the SLO alongside the comparison for this reason. The release itself needed two steps: first a version that reads both shapes but still writes the old one, then the new writer once no old reader remains.</p>\n\n<h2 id=\"cnry-argo-flagger-kayenta\">Wiring the comparison into a rollout controller</h2>\n\n<p>Argo Rollouts' Experiment step runs a baseline and a canary side by side, and with a traffic router it can give each a weight. The documented example sends 5% to each and leaves 90% on the old stack. Weighted experiments need the SMI, ALB or Istio traffic router. Without a router, Argo approximates weights by replica count, choosing the whole number of pods with the smallest difference and rounding up on a tie. By that rule, a 5% step on 10 replicas runs 1 canary pod, which is 10% of the pods.</p>\n\n<pre class=\"language-yaml\"><code class=\"language-yaml\"># Rollout (excerpt): stage 2 as a weighted experiment\nstrategy:\n  canary:\n    trafficRouting:\n      istio:\n        virtualService:\n          name: shop-vsvc\n    steps:\n    - experiment:\n        duration: 30m\n        templates:\n        - name: baseline\n          specRef: stable\n          weight: 5\n        - name: canary\n          specRef: canary\n          weight: 5\n        analyses:\n        - name: errors\n          templateName: canary-vs-baseline-errors\n          args:\n          - name: baseline-hash\n            value: \"{{templates.baseline.podTemplateHash}}\"\n          - name: canary-hash\n            value: \"{{templates.canary.podTemplateHash}}\"</code></pre>\n\n<p>The experiment's pods get pod-template hashes of their own, different from the Rollout's stable and canary ReplicaSets, which is what lets the query below select them. Argo's Kayenta page passes <code>podTemplateHashValue: Stable</code> and <code>Latest</code> instead, which selects the Rollout's ReplicaSets rather than the experiment's baseline. Copy the Experiment page's variables if you want the fresh baseline.</p>\n\n<pre class=\"language-yaml\"><code class=\"language-yaml\">apiVersion: argoproj.io/v1alpha1\nkind: AnalysisTemplate\nmetadata:\n  name: canary-vs-baseline-errors\nspec:\n  args:\n  - name: baseline-hash\n  - name: canary-hash\n  metrics:\n  - name: error-delta\n    initialDelay: 5m\n    interval: 5m\n    count: 5\n    # An empty result matches neither condition: Inconclusive, so the rollout pauses.\n    successCondition: len(result) &gt; 0 &amp;&amp; result[0] &lt;= 0.002\n    failureCondition: len(result) &gt; 0 &amp;&amp; result[0] &gt; 0.002\n    provider:\n      prometheus:\n        address: http://prometheus.monitoring:9090\n        # Assumes the scrape config copies the pod label rollouts-pod-template-hash\n        # onto each series as rollouts_pod_template_hash.\n        query: |\n          (\n            (sum(rate(http_requests_total{rollouts_pod_template_hash=\"{{args.canary-hash}}\",code=~\"5..\"}[5m])) or vector(0))\n            / sum(rate(http_requests_total{rollouts_pod_template_hash=\"{{args.canary-hash}}\"}[5m]))\n          )\n          -\n          (\n            (sum(rate(http_requests_total{rollouts_pod_template_hash=\"{{args.baseline-hash}}\",code=~\"5..\"}[5m])) or vector(0))\n            / sum(rate(http_requests_total{rollouts_pod_template_hash=\"{{args.baseline-hash}}\"}[5m]))\n          )</code></pre>\n\n<p>An AnalysisRun ends Successful, Failed or Inconclusive, which continue, abort or pause the rollout. The <code>or vector(0)</code> stops a canary with no 5xx series from emptying the ratio, while a cohort with no traffic still returns nothing, which matches neither condition and lands on Inconclusive instead of a pass. In the v1.10.1 source, <code>inconclusiveLimit</code> defaults to 0, so one such measurement pauses the rollout. The 5-minute rate window matches the interval, so no measurement reaches far behind the experiment. Read the analysis page's examples before copying them: its <code>error-rate</code> template uses <code>successCondition: result[0] &lt;= 0.95</code>, which passes until more than 95% of requests fail.</p>\n\n<p>Flagger's built-in checks, request success rate and duration, compare the canary's own metrics with a fixed <code>thresholdRange</code>, not with the primary; a delta needs a custom MetricTemplate. Stages are <code>stepWeight</code> up to <code>maxWeight</code>, or a list such as <code>stepWeights: [1, 2, 10, 80]</code>. Flagger gives rollback time as <code>interval × threshold</code>, so its example of 1 minute and 10 leaves a failing canary serving for ten minutes, the \"stopped after\" column in the budget output.</p>\n\n<p>Kayenta, Spinnaker's judge, does the statistics. In the 2026.1.3 source it runs a Mann-Whitney U test at 98% confidence. A metric must be statistically significant before its effect size is even checked, so the judge page notes that a 20% increase can still pass when the confidence interval is wide. Spinnaker's guidance asks for at least 50 data points per metric, and starting points of 3-hour canaries with a marginal score of 75 and a pass score of 95. One detail in the scorer: a metric group with no data at all scores 100, but if half or more of the metrics return no data, the whole canary fails.</p>\n\n<h2 id=\"cnry-k6-probe-canary-service\">A k6 probe for paths real traffic rarely reaches</h2>\n\n<p>The coupon path carried about 0.6% of the canary's traffic, so stage 2 saw 6 failures in a minute. The workbook does not count synthetic load as canarying, but it does suggest folding black-box checks into canary analysis. A k6 script aimed only at the canary pods, through the Rollout's <code>canaryService</code>, can push rare paths hard and quickly. Argo Rollouts treats a Kubernetes Job as a metric: exit code 0 passes, anything else fails.</p>\n\n<pre class=\"language-javascript\"><code class=\"language-javascript\">// canary-probe.js: a black-box check sent only to the canary pods (an Argo Rollouts Job metric).\nimport http from 'k6/http';\nimport { check } from 'k6';\n\nconst TARGET = __ENV.CANARY_URL || 'http://127.0.0.1:3481';\n\nexport const options = {\n  scenarios: {\n    probe: {\n      executor: 'constant-arrival-rate',\n      rate: 20,\n      timeUnit: '1s',\n      duration: '1m',\n      preAllocatedVUs: 10,\n    },\n  },\n  thresholds: {\n    'http_req_failed{step:coupon_order}': ['rate&lt;0.01'],\n    'http_reqs{step:coupon_order}': ['count&gt;=100'],\n    'http_req_duration{step:product}': ['p(95)&lt;100'],\n  },\n};\n\nexport default function () {\n  http.get(`${TARGET}/products/${Math.floor(Math.random() * 500)}`, { tags: { step: 'product' } });\n  // Exercise the path the production mix reaches rarely: every probe iteration sends a coupon order.\n  const res = http.post(`${TARGET}/orders`, JSON.stringify({ sku: 'SKU-1', qty: 1, coupon: 'PROBE-ONLY' }), {\n    headers: { 'Content-Type': 'application/json' },\n    tags: { step: 'coupon_order' },\n  });\n  check(res, { 'order placed': (r) =&gt; r.status === 201 }, { step: 'coupon_order' });\n}</code></pre>\n\n<p>Against the defective canary it sent 1,201 coupon orders in a minute and exited 99:</p>\n\n<pre class=\"language-text\"><code class=\"language-text\">    http_req_failed{step:coupon_order}\n    ✗ 'rate&lt;0.01' rate=100.00%\n\n    http_reqs{step:coupon_order}\n    ✓ 'count&gt;=100' count=1201\n\nlevel=error msg=\"thresholds on metrics 'http_req_failed{step:coupon_order}' have been crossed\"</code></pre>\n\n<p>Against the healthy version it exited 0. The <code>count</code> floor is part 2's safeguard against a tag that receives no samples, whose thresholds pass. The probe has two costs. The Job metric fails on any non-zero exit, so an exception in the script (exit 107) aborts a good release exactly as a real failure (exit 99) does; failing closed is defensible because rollback is cheap, but choose it deliberately. And the probe writes real orders. The workbook warns that artificial load on billing paths has real side effects, so use a test tenant the business has agreed to, or probe read paths only.</p>\n\n<h2 id=\"cnry-start-on-next-release\">Apply this to your next release</h2>\n\n<p>Check today whether your metrics can be split by version. Take one SLI with a version label, error ratio is the easiest, and write the comparison as canary against baseline in the same window, with a sample floor per cohort. Price your stage plan with the budget formula, using your real time to stop. Add an absolute SLO check on the control. Then send a deliberately broken build and a healthy one through staging under the same rules, and keep both decision records: a gate that has never aborted is untested. For which statuses count as failures, the <a href=\"/tools/http-status\">HTTP status code reference</a> helps; the workbook suggests leaving 4xx out of canary error counts and catching broken URLs with black-box probes.</p>\n\n<h2 id=\"cnry-faq\">Questions about canary analysis</h2>\n\n<h3 id=\"cnry-faq-low-traffic\">What if the service gets too little traffic for these sample floors?</h3>\n<p>Lengthen the stage rather than lowering the floor, and use the probe for the paths you care about. The workbook ties the size and duration a canary needs to request rate and how varied requests are: the more uniform the traffic, the less you need.</p>\n\n<h3 id=\"cnry-faq-one-at-a-time\">Can two canaries run at once?</h3>\n<p>The workbook is direct: \"We strongly advise running only one canary deployment at a time.\" Overlapping canaries contaminate each other's signals.</p>\n\n<h3 id=\"cnry-faq-off-peak\">Should canaries run off-peak to be safe?</h3>\n<p>Off-peak lowers the cost of a bad release and its chance of being caught. The workbook notes that performance defects typically appear only under heavy load, so an off-peak canary can pass a release that fails at peak.</p>\n\n<h2 id=\"cnry-references\">Primary references</h2>\n<ul>\n<li><a href=\"https://sre.google/workbook/canarying-releases/\">Canarying Releases, Google SRE workbook chapter 16</a>. Canary and control, the 5% at 20% example, budget cost proportional to exposure, gradual stages, attributable metrics, the risk of before/after evaluation, shared failure domains, aggregation intervals no longer than the canary, one canary at a time, and the limits of artificial load.</li>\n<li><a href=\"https://sre.google/workbook/alerting-on-slos/\">Alerting on SLOs, Google SRE workbook chapter 5</a>. Burn rate, and the 14.4× one-hour fast-burn page used to show why service-wide alerts miss a small canary.</li>\n<li><a href=\"https://sre.google/workbook/implementing-slos/\">Implementing SLOs, Google SRE workbook chapter 2</a>. The error budget as 100% minus the SLO, and the error budget policy.</li>\n<li><a href=\"https://argo-rollouts.readthedocs.io/en/stable/features/canary/\">Argo Rollouts canary strategy</a>. <code>setWeight</code> and <code>pause</code> steps, replica-count rounding without traffic routing, and <code>canaryService</code>.</li>\n<li><a href=\"https://argo-rollouts.readthedocs.io/en/stable/features/analysis/\">Argo Rollouts analysis and progressive delivery</a>. AnalysisRun phases and their effect on the rollout, <code>successCondition</code> and <code>failureCondition</code>, empty-result and NaN handling, Inconclusive runs, <code>initialDelay</code>, and the published <code>error-rate</code> example.</li>\n<li><a href=\"https://argo-rollouts.readthedocs.io/en/stable/features/experiment/\">Argo Rollouts Experiment CRD</a>. Baseline and canary ReplicaSets from <code>specRef</code>, weighted experiment steps for SMI, ALB and Istio, and separate pod-template hashes.</li>\n<li><a href=\"https://argo-rollouts.readthedocs.io/en/stable/analysis/job/\">Argo Rollouts Job metrics</a>. Exit code 0 passes, non-zero fails, and image-pull failures are Inconclusive.</li>\n<li><a href=\"https://github.com/argoproj/argo-rollouts/blob/v1.10.1/docs/analysis/kayenta.md\">Kayenta integration page in the Argo Rollouts repo at v1.10.1</a>. The example that passes the Stable and Latest hashes to the control and experiment scopes.</li>\n<li><a href=\"https://github.com/argoproj/argo-rollouts/blob/v1.10.1/pkg/apis/rollouts/v1alpha1/analysis_types.go\">Argo Rollouts analysis types at v1.10.1</a>. <code>inconclusiveLimit</code> defaulting to 0.</li>\n<li><a href=\"https://github.com/argoproj/argo-rollouts/releases/tag/v1.10.1\">Argo Rollouts v1.10.1 release</a>. Latest Argo Rollouts release when checked, published 2026-10-12.</li>\n<li><a href=\"https://docs.flagger.app/usage/deployment-strategies\">Flagger deployment strategies</a>. <code>stepWeight</code>, <code>maxWeight</code>, <code>stepWeights</code>, and rollback time as interval × threshold.</li>\n<li><a href=\"https://docs.flagger.app/usage/metrics\">Flagger metrics analysis</a>. The built-in success-rate and duration checks with fixed <code>thresholdRange</code>, and custom MetricTemplates.</li>\n<li><a href=\"https://github.com/fluxcd/flagger/releases/tag/v1.45.0\">Flagger v1.45.0 release</a>. The newest Flagger release on 2026-10-19.</li>\n<li><a href=\"https://spinnaker.io/docs/guides/user/canary/judge/\">How the Spinnaker canary judge works</a>. NetflixACAJudge: NaN handling, outlier removal, the Mann-Whitney U test at 98% confidence, effect-size thresholds and scoring.</li>\n<li><a href=\"https://spinnaker.io/docs/guides/user/canary/best-practices/\">Canary best practices from Spinnaker</a>. Comparing against an equivalent baseline deployed at the same time, at least 50 data points per metric, and starting thresholds of 75 and 95.</li>\n<li><a href=\"https://github.com/spinnaker/spinnaker/blob/spinnaker-release-2026.1.3/kayenta/kayenta-judge/src/main/scala/com/netflix/kayenta/judge/scorers/WeightedSumScorer.scala\">Kayenta weighted-sum scorer at Spinnaker 2026.1.3</a>. All-no-data groups scoring 100, and failure when half or more metrics return no data.</li>\n<li><a href=\"https://github.com/spinnaker/spinnaker/blob/spinnaker-release-2026.1.3/kayenta/kayenta-judge/src/main/scala/com/netflix/kayenta/judge/NetflixACAJudge.scala\">NetflixACAJudge source at Spinnaker 2026.1.3</a>. The classifier constructed with a 0.98 confidence level and 0.25 tolerance.</li>\n<li><a href=\"https://prometheus.io/docs/prometheus/latest/querying/operators/\">PromQL operators</a>. How <code>or</code> combines vectors, used to keep an absent error series from emptying the ratio.</li>\n<li><a href=\"https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/\">OpenTelemetry service attributes</a>. <code>service.version</code> as the version string of the service.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/using-k6/thresholds/\">Grafana k6 thresholds guide</a>. Thresholds as codified SLOs and the non-zero exit code when one fails.</li>\n<li><a href=\"https://github.com/grafana/k6/releases/tag/v2.3.0\">grafana/k6 at v2.3.0</a>. The version used for the lab traffic and the probe.</li>\n</ul>\n"}