Blue-Green vs Canary vs Rolling vs Shadow Deployments: Deployment Strategies Compared for Kubernetes and Beyond (2026)

Blue-Green vs Canary vs Rolling vs Shadow Deployments: Deployment Strategies Compared for Kubernetes and Beyond (2026)

Blue-Green vs Canary vs Rolling vs Shadow Deployments: Deployment Strategies Compared for Kubernetes and Beyond

Most teams pick a deployment strategy by copying the last conference talk they watched, then discover the real constraint months later during an incident. The deciding factor is rarely the traffic router. It is the database schema, the request volume available for statistics, and whether the new code is allowed to have side effects. That is the honest frame for blue green vs canary deployment, and for rolling updates and shadow traffic too.

This matters in 2026 because the plumbing has converged. Kubernetes Gateway API ships weighted backends and percentage-based request mirroring in its Standard channel, Amazon ECS added native blue/green, linear and canary modes in 2025, and progressive delivery controllers are mature. Every strategy is now a few lines of YAML away, so choosing wrongly is a design error rather than a tooling limit.

You will leave with a qualitative comparison table, working YAML for each strategy, the statistics that make low-traffic canaries lie, and a decision framework.

What this covers: the mechanics of each strategy, Kubernetes RollingUpdate semantics, Gateway API weights and mirroring, Argo Rollouts and Flagger analysis, AWS CodeDeploy and ECS, expand-contract schema changes, session draining, canary statistics, shadow side effects and failure modes.

Context and Background

Deployment strategies predate containers. Blue-green deployment was popularised in the early 2010s as a way to make releases boring: build a complete copy of production, test it, then flip the router. Canary releases borrowed the coal-mine metaphor, exposing a small slice of real users to the new version first. Rolling updates came from fleet management, replacing machines a few at a time. Shadowing, also called dark launching or traffic mirroring, grew out of large consumer platforms that needed to test new backends with real request shapes without anyone seeing the result.

Kubernetes made one of these the default. A Deployment object uses the RollingUpdate strategy unless you choose Recreate, so most clusters run rolling updates whether or not anyone decided to. The built-in controller has no notion of metrics, though. It only knows whether pods pass readiness probes. Anything smarter, such as pausing at 5% and checking error rates, needs a progressive delivery controller. Our Argo Rollouts vs Flagger ADR compares the two dominant open-source options in detail.

The traffic layer has also standardised. The Kubernetes Gateway API, the successor to Ingress, defines HTTPRoute with weighted backendRefs and a RequestMirror filter. Version 1.3.0, announced on the Kubernetes blog in June 2025, promoted percentage-based request mirroring to the Standard channel (Kubernetes blog: Gateway API v1.3.0). If you are still on ingress-nginx, our ingress-nginx retirement and Gateway API migration playbook covers the move.

Outside Kubernetes, AWS CodeDeploy has long offered blue/green for ECS and canary or linear traffic shifting for Lambda. In July 2025 Amazon ECS gained built-in blue/green deployments with lifecycle hooks and bake time, and in October 2025 added built-in linear and canary modes (AWS: ECS built-in blue/green).

Existing comparisons usually stop at “canary is safer, blue-green is faster”. My thesis is different. The four strategies are not rungs on a safety ladder. They are answers to four separate questions: can two versions run at once, can two versions share the data store, do you have enough traffic to measure, and can the new code run without touching the outside world. Answer those, and the choice mostly makes itself.

Blue Green vs Canary Deployment, Rolling and Shadow: How Each Strategy Works

Direct answer: Blue-green runs two full environments and switches all traffic at once, giving instant rollback at double the capacity cost. Canary sends a small, growing share of real traffic to the new version while metrics decide promotion. Rolling replaces instances in batches with no extra fleet. Shadow mirrors live requests to the new version and discards its responses.

Blue green vs canary deployment compared with rolling and shadow traffic paths through a gateway

Figure 1: How traffic reaches the new version under each strategy. Blue-green flips the whole fleet, canary splits by weight, rolling mixes pods behind one Service, and shadow copies requests to a version whose responses are thrown away.

Read the figure from the router outward. Every strategy relies on something that can choose a backend per request: a Kubernetes Service, a Gateway API implementation, a service mesh sidecar, a cloud load balancer, or DNS. What differs is the unit of switching. Blue-green switches environments. Canary switches a percentage of requests. Rolling switches pods. Shadow does not switch anything users see; it duplicates.

Blue-green: switch the environment, keep the old one warm

In blue-green, “blue” is the live environment and “green” is an identical environment running the new version. You deploy green, run smoke and integration tests against it through a preview endpoint, then repoint the production router. The old environment stays up for a while so that rollback is just pointing the router back.

The property you are buying is a near-atomic cutover plus a rollback measured in seconds. With Argo Rollouts the switch is a selector change on the active Service, and the old ReplicaSet is kept for scaleDownDelaySeconds, which defaults to 30 seconds per the Argo Rollouts blue-green documentation. Most teams raise that value considerably so they can roll back after the first real traffic arrives.

The price is capacity. For the duration of the overlap you run two full fleets. On a 40-node service that is 40 extra nodes, or a large burst against your cluster autoscaler. The second price is subtler: blue-green tests the new version against synthetic or preview traffic before cutover, and then exposes it to 100% of users at once. If a defect only appears under real traffic mix, every user hits it simultaneously. Blue-green shortens recovery; it does not shrink blast radius.

Canary: switch a fraction of requests and let metrics decide

A canary release sends a small weight, say 1% or 5%, to the new version, holds it while metrics accumulate, and then increases the weight in steps. Promotion should be decided by an automated analysis that compares the canary’s error rate, latency and saturation against thresholds or against a baseline. This is the core of progressive delivery: release as a controlled experiment instead of an event.

Canary minimises blast radius. If the new version fails 100% of its requests at a 5% weight, only 5% of users are affected for the length of one analysis interval. Flagger documents the time to rollback as interval * threshold, so a 1-minute interval with a threshold of 5 aborts within about five minutes (Flagger deployment strategies).

The costs are time, traffic and compatibility. A canary that walks 5, 10, 25, 50 and 100 percent with ten-minute holds takes nearly an hour. It only gives a signal if the canary slice receives enough requests to measure, which we return to below. And because old and new versions serve users concurrently for that whole period, they must both work against the same database, the same caches and the same message schemas.

Rolling update: switch pods in batches

A rolling update replaces old pods with new pods a few at a time behind a single Service. Because the Service load-balances across all ready endpoints, traffic shifts in proportion to pod counts. There is no separate router step. It is the default in Kubernetes and the cheapest strategy: the only extra capacity is the surge allowance.

Rolling is the strategy with the weakest controls. The Kubernetes Deployment controller gates progress on readiness probes, not on business metrics. A pod that answers its readiness probe but returns wrong prices will happily be rolled across the entire fleet. Rollback is another rolling update in reverse, so it takes about as long as the forward rollout. And mixed versions serve traffic for the whole duration, with no way to pin a user to one version unless you add session affinity.

Shadow: duplicate requests and discard the answers

A shadow deployment, also called traffic mirroring, copies some or all live requests to the new version. The primary still answers users. The shadow’s responses are dropped by the proxy, so users never see them. You compare the two versions offline through logs, metrics, or a diff service.

Shadowing gives the best pre-exposure signal of any strategy: real request shapes, real concurrency, and zero user-facing blast radius for the response path. It is ideal for rewrites of read-heavy services, new search ranking backends, or a migration from one database engine to another. The catch is side effects. A mirrored request that writes to the database, charges a card, sends an email or publishes an event does that twice. Flagger’s documentation is explicit that mirroring should be used only for requests that are idempotent or can safely be processed twice. Shadow is a testing technique that happens to use production traffic, not a release strategy on its own. You still need one of the other three to actually ship.

Deployment Strategies Comparison: Mechanics, YAML and the Numbers That Matter

The table below is the qualitative deployment strategies comparison I use in design reviews. The ratings are judgements, not measurements; your router, fleet size and observability stack move them. Treat them as defaults to argue with.

Dimension Rolling update Blue-green Canary Shadow (mirroring)
Blast radius when v2 is bad Grows with each batch; can reach 100% if probes pass 100% of users at cutover Bounded by current weight, e.g. 1 to 10% Near zero for responses; side effects can leak
Rollback time Slow; another rolling update in reverse Seconds; flip router back while old fleet is warm Fast; set weight to 0 Instant; remove mirror filter
Extra infrastructure cost Low; only the maxSurge pods High; up to 2x fleet during overlap Low to medium; canary pods plus analysis Medium to high; shadow fleet plus duplicated downstream load
Two versions serving at once Yes, for the whole rollout Briefly, or not at all Yes, for the whole analysis Yes, but only v1 answers users
DB migration compatibility Requires backward-compatible schema Requires compatible schema or careful sync; shared DB is the norm Requires backward-compatible schema Needs isolated or read-only data path for writes
Traffic needed for a signal None beyond readiness None before cutover Substantial; low traffic gives weak statistics Moderate; offline diffing tolerates lower volume
Typical tooling Kubernetes Deployment, ECS rolling Argo Rollouts, ECS native, CodeDeploy, DNS or LB switch Argo Rollouts, Flagger, Gateway API weights, ECS canary, CodeDeploy Gateway API RequestMirror, Istio mirror, Flagger mirror
Best fit Stateless services with good probes Releases needing atomic switch and instant rollback High-traffic user-facing services Rewrites, engine migrations, read-heavy paths

Two rows deserve emphasis. First, blast radius and rollback time are independent axes. Blue-green wins on rollback and loses on blast radius; canary wins on blast radius and is merely good on rollback. Second, the database row constrains three of the four strategies identically: if old and new code cannot share a schema, none of rolling, canary or a shared-database blue-green is safe.

Rolling update in Kubernetes: what maxSurge and maxUnavailable really do

For a rolling update in Kubernetes, two fields control pace. maxSurge is how many pods may exist above the desired replica count. maxUnavailable is how many may be unavailable below it. Both default to 25%. Per the Kubernetes API reference, the absolute value for maxSurge is calculated by rounding the percentage up, while maxUnavailable rounds down, and the two cannot both be zero.

Work through ten replicas with the defaults. maxSurge is ceil(2.5) = 3, so the Deployment may run up to 13 pods. maxUnavailable is floor(2.5) = 2, so at least 8 pods must stay available. The controller creates three new pods, waits for readiness, then removes old pods while keeping at least eight available, and repeats. The mixed-version window is the whole rollout.

Rolling update Kubernetes sequence showing ReplicaSet scale up, readiness, EndpointSlice changes and scale down

Figure 2: The rolling update loop. The Deployment controller scales the new ReplicaSet within maxSurge, waits for pods to become ready and join EndpointSlices, then scales the old ReplicaSet within maxUnavailable.

The figure hides the step where most “zero-downtime” rollouts actually fail: removal. When an old pod is deleted, it is removed from EndpointSlices and sent SIGTERM at roughly the same time, and these happen asynchronously across the cluster. Proxies and load balancers that have not yet observed the endpoint change keep sending requests to a pod that is shutting down. A preStop sleep of a few seconds and an application that stops accepting new connections gracefully but finishes in-flight ones are the standard fix. Our guide to Kubernetes graceful shutdown, PodDisruptionBudgets, preStop and SIGTERM walks through the timing.

Three settings change rolling behaviour materially. maxUnavailable: 0 with a positive maxSurge gives a surge-only rollout that never drops below desired capacity, which is the right default for latency-sensitive services. minReadySeconds makes a new pod count as available only after it has stayed ready for that long, which catches pods that crash shortly after start. progressDeadlineSeconds marks the rollout as failed if it makes no progress; the controller does not roll back automatically, so your pipeline must act on that condition.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
spec:
  replicas: 10
  minReadySeconds: 20          # pod must stay Ready 20s before it counts
  progressDeadlineSeconds: 600 # mark Progressing=False after 10 min stall
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2              # up to 12 pods during rollout
      maxUnavailable: 0        # never fewer than 10 available
  selector:
    matchLabels: { app: checkout }
  template:
    metadata:
      labels: { app: checkout }
    spec:
      terminationGracePeriodSeconds: 45
      containers:
        - name: app
          image: registry.example.com/checkout:2.4.0
          readinessProbe:
            httpGet: { path: /ready, port: 8080 }
            periodSeconds: 5
          lifecycle:
            preStop:
              exec: { command: ["sleep", "10"] }

With these values and ten replicas, the cluster temporarily needs capacity for 12 pods. That 20% surge is the entire infrastructure overhead of a rolling update, which is why it remains the default for most internal services.

Canary with Gateway API weights and an analysis loop

Gateway API expresses a traffic split as weights on backendRefs in an HTTPRoute. Per the Gateway API specification, each backend’s share is its weight divided by the sum of all weights, weights default to 1, and they do not need to sum to 100. A weight of 0 sends no traffic. You can run a manual canary with nothing more than this:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: checkout
spec:
  parentRefs:
    - name: public-gw
  hostnames: ["shop.example.com"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: /checkout }
      backendRefs:
        - name: checkout-stable
          port: 8080
          weight: 95
        - name: checkout-canary
          port: 8080
          weight: 5

Manual weight edits do not scale and do not enforce analysis. A progressive delivery controller automates the loop in Figure 3: set a weight, hold, query metrics, decide.

Progressive delivery canary analysis loop with weight steps, Prometheus queries, promotion and abort

Figure 3: The canary analysis loop. Each step sets a weight, collects metrics for a hold period, and an analysis run decides to continue, promote, abort to stable, or pause for a human when results are inconclusive.

In Argo Rollouts, the canary strategy lists steps, and an AnalysisTemplate defines metrics with a successCondition and a failureLimit. Argo’s analysis documentation distinguishes background analysis, which runs alongside the steps, from inline analysis steps that block the rollout until they complete. A pause with no duration waits indefinitely for a promote command. Gateway API support comes through the Argo Rollouts Gateway API traffic router plugin, which must be registered with the controller.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: checkout
spec:
  replicas: 10
  selector:
    matchLabels: { app: checkout }
  template:
    metadata:
      labels: { app: checkout }
    spec:
      containers:
        - name: app
          image: registry.example.com/checkout:2.4.0
  strategy:
    canary:
      stableService: checkout-stable
      canaryService: checkout-canary
      trafficRouting:
        plugins:
          argoproj-labs/gatewayAPI:
            httpRoute: checkout
            namespace: shop
      analysis:                      # background analysis
        templates:
          - templateName: success-rate
        startingStep: 1
        args:
          - name: service
            value: checkout-canary
      steps:
        - setWeight: 5
        - pause: { duration: 10m }
        - setWeight: 25
        - pause: { duration: 10m }
        - setWeight: 50
        - pause: {}                  # wait for manual promote
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  args:
    - name: service
  metrics:
    - name: success-rate
      interval: 2m
      failureLimit: 2
      successCondition: result[0] >= 0.995
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{service="{{args.service}}",code!~"5.."}[2m]))
            /
            sum(rate(http_requests_total{service="{{args.service}}"}[2m]))

Without a traffic router, Argo Rollouts approximates weights with replica counts. Its canary documentation gives the example that with 10 replicas and a 10% weight, the canary gets one pod and stable keeps nine. That is fine for coarse steps and useless for a 1% canary on a 10-replica service, which is why serious canaries pair the controller with a Gateway API implementation, a service mesh, or a cloud load balancer that can split by request rather than by pod.

Flagger takes a different shape: you declare a Canary resource that wraps an existing Deployment and Flagger generates primary and canary workloads. Its analysis block uses interval, threshold, maxWeight and stepWeight, or an ordered stepWeights list for non-linear promotion. The Argo Rollouts vs Flagger ADR compares the operational models; for this article, the point is that both implement the same loop.

Canary statistics: why a 1% canary on low traffic tells you almost nothing

The quiet failure of canary analysis is statistical. Suppose a service has a baseline error rate of 0.1% and the new version doubles it to 0.2%. That is a serious regression. How many requests must the canary see before the difference is detectable?

A standard two-proportion sample size calculation, with a 5% false-positive rate and 80% power, needs roughly 23,000 requests in each group to distinguish 0.1% from 0.2%. This is an illustrative calculation using the normal approximation, not a measured benchmark, but the order of magnitude is the point. At 100 requests per second total with a 1% canary weight, the canary sees one request per second. Collecting 23,000 requests takes about six and a half hours. A ten-minute step at that weight observes about 600 requests, in which you expect 0.6 errors from a healthy canary and 1.2 from a broken one. The analysis cannot tell them apart.

There are four practical responses. Raise the weight early for low-traffic services, accepting more blast radius in exchange for signal. Lengthen holds, which slows delivery. Use metrics with more signal per request, such as latency percentiles or business conversion events rather than rare errors. Or compare canary against a baseline deployed at the same time with the same weight, rather than against the long-lived stable fleet. Netflix’s open-source Kayenta judge, used with Spinnaker, popularised the baseline-versus-canary comparison using nonparametric statistics because stable fleets carry warm caches and long-lived connections that bias a naive comparison.

Two further pitfalls bite often. Absolute thresholds such as success-rate >= 0.995 evaluated on tiny samples flap: a single error among 150 requests reads as 99.3% and fails the step. And repeated checks inflate false positives. If an analysis evaluates every two minutes over an hour, that is 30 chances to trip a 5% false-positive test. Use failureLimit greater than zero, require consecutive failures, and set a minimum request count below which the result is inconclusive rather than failed.

Blue-green with Argo Rollouts: preview, gate, switch

Argo Rollouts implements blue-green by managing two Services. The activeService selects the live ReplicaSet and the previewService selects the new one. Per the blue-green documentation, autoPromotionEnabled defaults to true, scaleDownDelaySeconds defaults to 30, and prePromotionAnalysis and postPromotionAnalysis can gate the switch on an analysis run. previewReplicaCount lets you run a smaller preview stack until promotion, which trims the capacity bill during testing.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: pricing
spec:
  replicas: 8
  selector:
    matchLabels: { app: pricing }
  template:
    metadata:
      labels: { app: pricing }
    spec:
      containers:
        - name: app
          image: registry.example.com/pricing:3.1.0
  strategy:
    blueGreen:
      activeService: pricing-active
      previewService: pricing-preview
      previewReplicaCount: 2          # small preview stack before promotion
      autoPromotionEnabled: false     # require explicit promote
      prePromotionAnalysis:
        templates:
          - templateName: smoke-tests
      postPromotionAnalysis:
        templates:
          - templateName: success-rate
        args:
          - name: service
            value: pricing-active
      scaleDownDelaySeconds: 1800     # keep blue warm 30 min for rollback

The postPromotionAnalysis is the part most blue-green setups skip and should not. Pre-promotion tests prove the new version works on synthetic traffic. Post-promotion analysis, run while the old ReplicaSet is still warm, is what turns a real-traffic regression into a seconds-long rollback rather than a fresh deployment.

On AWS, the same pattern appears with different nouns. CodeDeploy’s ECS blue/green uses two target groups behind a load balancer and an AppSpec file with lifecycle hooks; its predefined ECS configurations include CodeDeployDefault.ECSCanary10Percent5Minutes, CodeDeployDefault.ECSLinear10PercentEvery1Minutes and CodeDeployDefault.ECSAllAtOnce, according to the CodeDeploy deployment configurations guide. The same guide notes that with a Network Load Balancer only the all-at-once configuration is supported.

# CodeDeploy AppSpec for an ECS blue/green deployment
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: "arn:aws:ecs:region:acct:task-definition/pricing:42"
        LoadBalancerInfo:
          ContainerName: "app"
          ContainerPort: 8080
Hooks:
  - AfterAllowTestTraffic: "ValidateGreenOnTestListener"
  - AfterAllowTraffic: "PostCutoverHealthCheck"

Amazon ECS now also offers these strategies natively, without CodeDeploy. The July 2025 blue/green launch added lifecycle hooks and a bake time during which instant rollback is available, supporting Application Load Balancer, Network Load Balancer and ECS Service Connect. The October 2025 update added linear and canary modes with step bake times for Application Load Balancer and Service Connect services (AWS: ECS built-in linear and canary). If you run ECS today, the native mode removes a moving part; CodeDeploy remains relevant for Lambda and EC2 targets.

Shadow deployment traffic mirroring with Gateway API

Shadow deployment traffic mirroring is a filter on an HTTPRoute rule. The RequestMirror filter names a backendRef and, since Gateway API v1.3.0, accepts either percent or a fraction with numerator and denominator; if neither is set, 100% of requests are mirrored. The spec requires mirrored requests to go to a single destination endpoint within that backend.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: search
spec:
  parentRefs:
    - name: public-gw
  rules:
    - matches:
        - path: { type: PathPrefix, value: /search }
          method: GET                 # mirror reads only
      filters:
        - type: RequestMirror
          requestMirror:
            backendRef:
              name: search-v2-shadow
              port: 8080
            percent: 10
      backendRefs:
        - name: search-v1
          port: 8080

Note the method: GET match. Restricting mirroring to read requests at the route level is the cheapest defence against duplicated side effects. Istio expresses the same idea with mirror and mirrorPercentage on a VirtualService, and Flagger can drive mirroring through analysis.mirror: true, with mirrorWeight applying only on Istio according to its documentation. Mirroring is fire-and-forget in common proxy implementations: the proxy does not wait for the shadow response, so a slow shadow should not add latency to users, but it still consumes connection pools and upstream capacity.

Databases: expand-contract is the real compatibility contract

Every strategy except a fully isolated blue-green runs old and new code against the same database at the same time. That makes the schema the hidden coupling. A migration that renames a column breaks whichever version does not know the new name, and a rollback after a destructive migration has nowhere to roll back to.

The standard answer is expand-contract, also called parallel change. In the expand phase you add new columns, tables or indexes without removing anything, and deploy code that writes both shapes. Backfill existing rows. Then deploy code that reads only the new shape. Only after the old code is gone, and after you are confident you will not roll back, run the contract phase that drops the old columns. Each phase is its own release, which makes every intermediate state compatible with its neighbours. Our deep dive on zero-downtime database schema migrations with expand-contract, pgroll and gh-ost covers the tooling.

Blue-green with two databases is sometimes proposed to avoid this. In practice it trades a schema problem for a replication problem: writes accepted by green during the overlap must reach blue for rollback to be lossless. Bidirectional replication between different schema versions is harder than expand-contract, so most teams keep one database and apply expand-contract regardless of strategy.

Message schemas follow the same rule. If version 2 publishes events in a new format, consumers still on version 1, and version 1 producers still running during a canary, must interoperate. Additive, optional fields and a schema registry with compatibility checks make that safe.

Sessions, connections and draining

The last mechanism is state held in connections. Rolling updates and canaries move requests, not sessions. If a user’s shopping cart lives in pod memory, a canary weight change can move them to a pod that has never seen them. Externalise session state to a shared store, or use consistent hashing on a session cookie so that a user sticks to one version. Sticky canaries also improve analysis quality, because a user who flips between versions mid-session generates errors that neither version owns.

Long-lived connections complicate cutovers further. A blue-green switch on the router changes where new connections go; existing WebSocket or gRPC streams stay on blue until they close. Plan for that by setting connection maximum ages on servers, sending GOAWAY or close frames during drain, and keeping blue alive at least as long as your longest expected stream. Otherwise “instant cutover” quietly takes hours, and scaling blue down early drops those users.

Trade-offs, Gotchas, and What Goes Wrong

Every strategy has a characteristic failure, and most incidents I have seen trace back to one of these.

Readiness that lies. Rolling updates and replica-based canaries trust the readiness probe. A probe that returns 200 because the HTTP server started, while the database pool or a downstream dependency is broken, lets bad pods into rotation at full speed. Make readiness check what the pod needs to serve real requests, but never call shared dependencies so aggressively that one outage takes every pod out of rotation at once.

Shadow traffic with side effects. This is the most dangerous gotcha in shadow deployment traffic mirroring. The mirrored request carries real user identifiers and real payloads. If the shadow writes to the production database, calls a payment provider, sends email or produces to a production Kafka topic, users get double charges and duplicate notifications. Figure 4 shows the isolation pattern that makes shadowing safe.

Shadow deployment traffic mirroring isolation with shadow database copy, stubbed external APIs and a diff service

Figure 4: Safe shadowing. The gateway mirrors a percentage of requests to the shadow, which writes only to a copy of the data, calls stubbed external APIs, and reports its outputs to a diff service that compares them with the primary’s.

Even with isolation, shadows mislead. A shadow writing to a copy of the database diverges from production within minutes, so later reads return different results for reasons unrelated to the code under test. Authentication tokens with one-time nonces, rate limits keyed by user, and idempotency keys all behave differently when a request is seen twice. Diff services must tolerate timestamps, generated IDs and ordering differences, or engineers learn to ignore the noise.

Mirrored load is real load. Mirroring 100% of traffic doubles the request rate on every shared downstream, including caches, authorisation services and third-party APIs that you forgot to stub. Start at a low percentage and treat the shadow’s dependency graph as part of the experiment.

Canary metric pollution. If canary and stable pods report metrics under the same labels, analysis compares the fleet with itself. Make sure the version or rollout hash is a label on every metric used in analysis, and that dashboards break down by it. Also beware of shared-cause failures: a downstream outage during the canary step fails both versions, and an analysis that only looks at the canary will abort a healthy release.

Blue-green surprises. Scheduled jobs and queue consumers run in both colours unless you gate them, so a cron job fires twice. Caches warmed on blue are cold on green, so latency spikes right after cutover and can trigger a false rollback. DNS-based blue-green is subject to resolver caching beyond the TTL you set, so the cutover is neither instant nor complete.

Rollback that is not a rollback. Rolling back code does not roll back data. If version 2 wrote rows in a new format or ran a contract migration, reverting to version 1 can corrupt or misread them. This is why the contract step of expand-contract must lag the release it supports by at least one full rollback window.

Progress deadlines without action. Kubernetes sets a Deployment’s Progressing condition to false after progressDeadlineSeconds, but it does not roll back on its own. A pipeline that only runs kubectl apply and exits leaves a half-rolled fleet in place indefinitely. Use kubectl rollout status with a timeout and act on failure, or delegate to a controller that aborts automatically.

Practical Recommendations

Decide with four questions rather than a favourite tool. Can two versions safely run at once against the same data? If not, fix the schema with expand-contract before choosing anything, because every strategy except a fully isolated blue-green depends on it. Is there enough traffic for statistics? If a 5% slice cannot produce a few thousand requests per step, canary analysis gives weak evidence; prefer blue-green with strong post-promotion analysis, or canary with larger weights. Does the change carry high correctness risk on real request shapes, such as a rewrite or engine migration? Shadow it first, reads only. Is the service internal, stateless and low risk? A well-tuned rolling update is enough.

When the debate narrows to blue green vs canary deployment, let traffic volume break the tie: canary needs requests to measure, blue-green needs capacity to duplicate. My defaults for 2026 are simple. User-facing, high-traffic services use canary through a progressive delivery controller with Gateway API or a mesh doing the split. Low-traffic but critical services use blue-green with manual promotion and a long scale-down delay. Internal services use surge-only rolling updates. Shadowing is a pre-release phase for risky rewrites, not a standing practice. Pair all of them with feature flags, so that the deploy ships dark code and the release happens per segment; our feature flag architecture guide with OpenFeature shows how flags and deployments divide the work. Tie abort thresholds to the service’s error budget, as described in our post on SLO error budgets and burn-rate alerting.

Checklist before your next rollout:

  • [ ] Schema and message changes are expand-only for this release; contract is scheduled later.
  • [ ] Readiness probes reflect real serving ability; preStop and graceful shutdown are tested.
  • [ ] Every analysis metric is labelled by version and has a minimum sample size.
  • [ ] Rollback is automated and the old version stays warm for a defined window.
  • [ ] Shadow routes are read-only or fully isolated from production side effects.
  • [ ] Long-lived connections have a maximum age so draining completes.

Frequently Asked Questions

What is the main difference between blue-green and canary deployment?

Blue-green switches all traffic from the old environment to a fully provisioned new environment in one step, so rollback is a fast switch back but every user meets the new version at once. Canary shifts a small, growing percentage of real traffic to the new version and uses metrics to decide each step, so a defect reaches only a fraction of users. In short, blue-green optimises recovery time and canary optimises blast radius. Many teams combine them: a canary for exposure control, with the previous version kept warm for blue-green style rollback.

Is a Kubernetes rolling update the same as a canary release?

No. A rolling update replaces pods in batches based on maxSurge and maxUnavailable and only checks readiness probes before continuing. It has no pause at a fixed traffic percentage and no metric-based decision, so a version that passes readiness but misbehaves will roll across the whole fleet. A canary holds at defined weights and evaluates error rates, latency or business metrics before promoting. You can approximate a canary by pausing a rolling update, but tools such as Argo Rollouts or Flagger exist precisely to add the analysis and automated abort that Deployments lack.

When should I use shadow deployment instead of canary?

Use shadowing when you need to test a new version against real production request patterns without any user seeing its responses, for example a rewritten search service, a new recommendation model or a migration to a different database engine. It works best for read-heavy, idempotent paths. Avoid it for write paths unless the shadow is fully isolated with a copied datastore and stubbed external calls. Shadowing does not ship anything; after the shadow phase you still release with a canary, blue-green or rolling strategy.

How much traffic does a canary need to be statistically meaningful?

It depends on the baseline error rate and the size of regression you want to catch. As an illustrative calculation, detecting a jump from 0.1% to 0.2% errors with conventional 5% significance and 80% power needs on the order of 23,000 requests per group. At one canary request per second that is several hours. Low-traffic services should use larger canary weights, longer holds, higher-signal metrics such as latency, or compare against a same-age baseline rather than the long-running stable fleet.

Does blue-green deployment double my infrastructure cost?

During the overlap window it can, because two full fleets run at once. The total cost depends on how long the overlap lasts. If green is provisioned shortly before cutover and blue is scaled down after a 30 to 60 minute rollback window, the extra cost is a few hours of capacity per release. Argo Rollouts’ previewReplicaCount lets you test on a smaller preview stack, and cluster autoscaling or serverless capacity on ECS with Fargate reduces idle spend, but you must have quota for the peak.

How do I handle database migrations with zero-downtime deployments?

Use the expand-contract pattern. First add new schema elements without removing old ones and deploy code that works with both. Backfill data, then switch reads to the new shape. Only after the old code version is gone and the rollback window has passed, remove the old columns or tables in a separate release. This keeps every running version compatible with the schema, which rolling updates, canaries and shared-database blue-green all require. Never couple a destructive migration to the same release that first uses the new schema.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *