What each deployment strategy actually does

Every deployment strategy answers the same question: when the new version is wrong, who sees it, and for how long? The mechanics differ in how they limit that exposure.

A rolling update replaces instances in batches inside one environment. Old and new versions serve production traffic side by side until the rollout finishes. Two settings pace it. A surge limit sets how many extra instances may exist during the rollout. An unavailability limit sets how many may be out of service at once. Readiness checks decide when a new instance receives traffic and when the next batch starts. A defect therefore reaches a growing slice of all users, and rollback is another rolling replacement.

A blue-green deployment runs two complete environments. Blue serves traffic while green is deployed and tested in isolation. A single switch at the router, DNS record or load balancer moves all traffic at once. If green fails, everyone sees the failure, but reverting is the same switch flipped back, provided blue is still running.

A canary release sends a small, controlled share of traffic, or a defined user segment, to the new version. Its error rates and latency are compared against the baseline before you widen exposure. A defect reaches few users, and you find out from real traffic.

ROLLING              BLUE-GREEN             CANARY
 LB                   LB                     LB
 ├─> v1               └─> [switch]           ├─ 95% ─> v1 (baseline)
 ├─> v1 → v2              ├─> blue  (v1)     └─  5% ─> v2 (canary)
 └─> v2                   └─> green (v2)
 batches swap         all traffic moves      compare, then widen
ROLLING UPDATE            BLUE-GREEN                CANARY
(one environment)         (two environments)        (split traffic)

   +---------------+         +---------------+         +---------------+
   | load balancer |         | load balancer |         | load balancer |
   +-------+-------+         +-------+-------+         +-------+-------+
           |                         |                         |
           |                         | switch                95% |     5%
   +-------+-------+         +-------+.......+         +-------+-------+
   |       |       |         |               :         |               |
   v       v       v         v               v         v               v
 +----+  +----+  +----+    +-------+   +-------+     +--------+  +--------+
 | v1 |  | v2 |  | v2 |    | blue  |   | green |     |   v1   |  |   v2   |
 +----+  +----+  +----+    |  v1   |   |  v2   |     |baseline|  | canary |
                           +-------+   +-------+     +--------+  +--------+
 old + new serve together   blue live,  green idle    compare vs baseline
 readiness paces batches    revert = flip back        then widen the share

Traffic flow for each strategy: a rolling update swaps instances in batches behind one load balancer, blue-green moves all traffic between two full environments with a single switch, and canary splits a small share to the new version for comparison against the baseline.

How to implement them: rolling updates, switches and traffic splitting

Rolling updates

A Kubernetes Deployment with strategy.type: RollingUpdate uses two settings to control how fast a rollout moves. maxSurge is how many extra pods may exist above the desired replica count. maxUnavailable is how many pods may be missing below it. With 10 replicas, maxSurge: 2 and maxUnavailable: 0, capacity never falls below 10 pods, and you pay for up to 12 during the rollout. Raising maxUnavailable makes the rollout faster but means running with fewer pods while it happens. The readinessProbe is what holds the rollout back. A new pod gets no traffic and does not count as available until its probe passes, so the next batch waits for it. If the probe only checks that the process is up, a broken version will roll out at full speed.

Blue-green switches

Run v2 as a complete second Deployment, then change the Service selector (for example, version: blue to version: green) or the load balancer's target group. The switch takes effect quickly, but existing connections stay on the old pods until they close. Keep blue running at full size until you trust green. Rolling back means changing the selector back, which only works if the old pods are still warm.

Traffic splitting

With a replica-count canary, both Deployments sit behind one Service and share its label. One v2 pod among ten pods gets roughly 10% of traffic, and the split is only approximate. You can't get 1% without running 100 pods, and you can't route by user or header. Exact weights and header-based rules need an ingress controller or a service mesh.

Draining

Pods that are being removed need time to finish their work. Set terminationGracePeriodSeconds and a preStop delay so the pod is taken out of the endpoints before it shuts down. Plan explicitly for sticky sessions, WebSockets, long-lived gRPC streams and background jobs. These can outlast any reasonable grace period, so they need to reconnect cleanly, hand off their work, or be checkpointed so they can resume.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
spec:
  replicas: 10
  selector:
    matchLabels:
      app: checkout
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2          # allow up to 12 pods during the rollout (extra cost, no capacity loss)
      maxUnavailable: 0    # never drop below 10 available pods
  minReadySeconds: 10      # a new pod must stay ready this long before it counts as available
  template:
    metadata:
      labels:
        app: checkout
    spec:
      # Total time allowed for preStop plus graceful shutdown before the pod is force-killed
      terminationGracePeriodSeconds: 60
      containers:
        - name: app
          image: registry.example.com/checkout:2.4.0
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /ready     # should check that the app can serve requests, not just that the process is up
              port: 8080
            periodSeconds: 5
            failureThreshold: 3
          lifecycle:
            preStop:
              exec:
                # Wait while the pod is removed from Service endpoints, then the app gets SIGTERM
                # and uses the rest of the grace period to finish in-flight requests and streams
                command: ["sh", "-c", "sleep 10"]

A rolling-update Deployment where maxSurge and maxUnavailable keep capacity at 10 pods, the readinessProbe decides when the rollout can move on, and a preStop delay inside the termination grace period lets old pods drain before they stop.

Version skew and database changes: the constraints every strategy shares

All three strategies run two versions of your code at the same time. Rolling updates and canaries do this by design, with old and new pods serving side by side for minutes or hours. Blue-green looks like an atomic switch, but in-flight requests, cached responses, queued messages and clients holding old payloads all cross the cutover. Every API contract, message schema and session format therefore has to be backward compatible (new code reads old data) and forward compatible (old code tolerates new data, for example by ignoring unknown fields).

The database is where this matters most. Blue-green gives you two fleets, not two databases. Suppose green writes rows in a new format or runs a destructive migration, such as dropping or renaming a column. Switching back to blue then sends traffic to code that cannot read the current schema, so the rollback fails exactly when you need it. The same applies to queues and caches, which are also shared. A new version that floods a queue, poisons a cache key or holds table locks degrades the old version too.

The standard fix is expand and contract, spread across several deploys:

  1. Add the new column as nullable. Old code ignores it.
  2. Deploy code that writes to both the old and new columns.
  3. Backfill existing rows in batches.
  4. Switch reads to the new column.
  5. Once no running version reads the old column, drop it.

You can roll back safely at every step except the last, which you run only after rollback is no longer needed.

-- Goal: replace orders.amount (NUMERIC dollars) with orders.amount_cents (BIGINT).
-- Each phase ships as its own deploy; blue/green or old/new pods can run between any two.

-- Phase 1 (expand): add the new column as nullable. Old code never references it.
ALTER TABLE orders ADD COLUMN amount_cents BIGINT;

-- Phase 2 (dual-write): app code only, no DDL. New version writes both columns:
--   INSERT INTO orders (id, amount, amount_cents) VALUES ($1, $2, $3);
-- Old version still writes only amount, so rows can arrive with amount_cents NULL.

-- Phase 3 (backfill): convert existing rows in small batches to keep locks short.
-- Re-run until it reports 0 rows updated; it also catches rows old pods wrote.
UPDATE orders
SET amount_cents = ROUND(amount * 100)
WHERE id IN (
  SELECT id FROM orders
  WHERE amount_cents IS NULL
  LIMIT 1000
);

-- Phase 4 (switch reads): deploy code that reads amount_cents but keeps dual-writing,
-- so rolling back to the phase-2 version still finds amount populated.
-- Once no old pods remain, enforce the invariant:
ALTER TABLE orders ALTER COLUMN amount_cents SET NOT NULL;

-- Phase 5 (contract): only after every running version, and any version you might
-- roll back to, has stopped reading and writing amount. This step cannot be undone.
ALTER TABLE orders DROP COLUMN amount;

A PostgreSQL expand-and-contract migration split into deploy phases. Rollback stays safe through phase 4, and the destructive DROP waits until no version you could roll back to still uses the old column.

Defining promotion and rollback criteria before you deploy

…the old column is no longer needed.

Decide what "healthy" means before the rollout starts. Otherwise you end up judging it from a dashboard mid-deploy, hoping things look fine. Choose a few signals tied to user impact: error rate, high-percentile latency, and saturation (CPU, memory, connection pool usage). For each one, set a threshold relative to the current version and an observation window long enough to get past the startup spike.

None of this works without per-version observability. If metrics are aggregated across the whole service, a canary taking 5% of traffic can double its error rate and barely move the combined graph. Label every metric with the version so you can compare canary against baseline directly. Without that, a passing canary only proves nothing failed loudly.

Low-traffic services have a sampling problem. A small share of a few requests per minute cannot separate a regression from noise. Lengthen the window, raise the canary share, or add synthetic traffic.

Some failures a canary will miss:

  • effects that appear only at full load
  • bugs that affect one tenant who never hits the canary
  • slow memory leaks
  • connection pool exhaustion
  • behaviour that changes once caches warm up

Automated promotion carries its own risks. Noisy metrics, poor thresholds, or missing data read as "zero errors" can promote a bad build or abort a good one. Replay analysis rules against past incidents, treat absent data as a failure, and keep a manual override.

Example abort criteria, with illustrative values: roll back if the new version's error rate exceeds the old version's by more than half a percentage point for five consecutive minutes, if its p99 latency runs 20% higher over ten minutes, or if its metrics stop arriving.

Impact on infrastructure cost and engineering time

Those thresholds cost time and money to enforce, and the strategies differ sharply on both.

Illustrative estimate (assumed numbers, not a benchmark). Assume a service of N = 20 instances at steady state. Peak overhead = (peak instances − N) / N.

Strategy Assumed extra instances at peak Peak instances Peak overhead Main engineering cost
Rolling update surge = 5 25 5 / 20 = 25% Low: built into most orchestrators
Canary 1–2 21–22 5–10% Traffic routing, per-version metrics, analysis tooling
Blue-green full second set = 20 40 20 / 20 = 100% Maintaining and validating two environments

All of this overhead is temporary. Blue-green's doubling lasts only as long as you keep the idle environment around. If you hold it for fast rollback, you pay for it all the time.

The engineering cost runs in a different order from the capacity cost. Rolling updates are the cheap default because the orchestrator does the work, and Kubernetes ships it as standard Deployment behaviour. Blue-green adds environment management: keeping configuration, secrets and dependencies identical across both sides so the switch holds no surprises. Canaries are cheap in instances but the most expensive to build. You need weighted routing, metrics labelled by version, and something that compares those metrics against the thresholds above.

Each strategy also saves time somewhere:

  • Blue-green rollback is a traffic switch rather than a redeploy, so incidents end sooner.
  • Automated canary analysis removes the engineer watching dashboards during every release.
  • A small canary blast radius means a bad release reaches 5% of users, not all of them, so the incident is smaller.

When to use each strategy, and when to avoid it

The choice comes down to three questions: how many users can a bad release reach, how fast must you undo it, and can your metrics tell the new version from the old one?

Rolling update Blue-green Canary
Blast radius Grows each batch, up to the full fleet Full fleet at cutover Canary slice only
Rollback speed Slow: another rolling update Near-instant while old environment is kept Fast: drop canary weight to zero
Extra capacity One surge batch Roughly a second full environment Canary instances only
Mixed-version exposure Throughout the rollout Brief, at switch time Throughout the analysis window
Infrastructure needs Orchestrator with readiness checks Switchable router, duplicate environment Weighted routing, per-version metrics
Best fit Stateless, backward-compatible services Whole-fleet cutover, pre-cutover smoke tests High-risk changes on high-traffic services

Rolling updates are the low-overhead default for stateless services where old and new versions coexist safely. Avoid them when a defect must not reach many instances. By the time health checks or users surface the problem, several batches may already run it, and undoing it takes another rollout.

Blue-green fits releases that must flip the whole fleet at once, or that you want to smoke-test on production infrastructure before any user sees them. Avoid it when you can't afford duplicate capacity, or when the release includes irreversible changes such as destructive migrations. Every user hits the new version at once, and switching back doesn't undo the data changes.

Canaries fit risky changes on services with enough traffic to produce meaningful error-rate and latency differences within minutes. Avoid them for low-traffic services, where a small slice yields too few requests to judge. Also skip them if your team can't split dashboards by version, because you can't detect a regression you can't attribute.

Combining strategies and other alternatives

The three strategies are not mutually exclusive. A common pattern runs a canary step first: route a small share of traffic to a handful of new instances, check the version-split metrics, then hand off to a rolling update for the rest of the fleet. The canary catches regressions early, and the rolling update finishes the job without a second full environment. Blue-green also combines well with progressive traffic shifting. Instead of flipping 100% of traffic to green at once, shift it in steps while blue stays warm. You keep the instant rollback target and also limit how many users see a bad release.

Recreate

Recreate stops every old instance before starting new ones, so the service is down for a short period. Kubernetes Deployments support it as a built-in strategy type. It fits internal tools, batch workers and services that cannot run two versions at once, such as those holding an exclusive lock or running an incompatible schema migration, when the downtime is acceptable.

Feature flags

Flags separate deploying code from releasing behavior. You ship the code dark, then enable it per user, tenant or percentage. Use flags when the risk lies in a specific feature rather than the whole build, or when product teams need to control timing. They add cleanup work, because stale flags accumulate as dead branches.

Shadow traffic

Mirroring copies live requests to the new version and discards its responses. It suits read-heavy services and rewrites where you want production load without any user impact. Writes need isolation or stubbing so they don't produce duplicate side effects.

How OneEnv can help

Disclosure: OneEnv is the product behind this blog.

OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.

Applied to deployment strategies, this is where it helps:

Saves time

  • Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
  • One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
  • Reviewers see the exact diff and the list of affected services up front, so approval is a decision rather than an investigation.

Saves cost

  • Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
  • Less duplicated configuration to maintain: one shared definition inherited by many services.
  • Per-service approve/reject means a bad shared value can be refused where it would break something, before it is live.
  • OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.

These are qualitative benefits, not measured results — how much you save depends on how many services share the value.

Want to try it? Start at oneenv.io.