Deployment Strategies & Progressive Delivery
Progressive delivery replaces the all-at-once production push with a controlled, observable, reversible rollout β new code reaches users in slices that can be halted or reversed the instant a metric moves the wrong way. This guide is for platform engineers and release owners who need to ship frontend and full-stack applications frequently without trading away production stability.
Architecture Overview
Every progressive delivery strategy is a variation on the same control loop: deploy the new version without exposing it, verify it against health signals, shift traffic incrementally, and promote or roll back based on what the metrics say. The diagram below shows that loop end to end.
The three infrastructure strategies β blue-green, canary, and rolling β differ only in how the traffic slice is shifted. Feature-flag-driven release management sits on top of all of them, moving the exposure decision from infrastructure into application code.
Core Concepts
| Term | Definition | Deep dive |
|---|---|---|
| Blue-green deployment | Two identical environments; traffic flips atomically from the live one to the idle one after validation | Blue-Green Deployments for Full-Stack Apps |
| Canary release | A small, growing percentage of traffic is routed to the new version while metrics are analysed | Canary Releases & Progressive Rollouts |
| Feature flag | A runtime toggle that controls whether shipped code is exposed to a user, decoupling deploy from release | Feature-Flag-Driven Release Management |
| Atomic deploy | A static release made live in a single indivisible operation (symlink or manifest swap), never a half-copied directory | See Pattern 1 below |
| Automated rollback | A metric-triggered reversal to the last known-good version without human intervention | Automated Rollback Triggers & Runbook Integration |
| Analysis window | The fixed observation period during which a canaryβs metrics are compared to the stable baseline before promotion | See Pattern 2 below |
Pattern 1 β Atomic Blue-Green Cutover (Foundational)
When to use it: Any service where you can briefly run two versions and need an instant, reversible cutover. This is the baseline production-grade strategy and the simplest to reason about.
Blue-green keeps two environments β one live (blue), one idle (green). You deploy to the idle one, validate it in isolation, then flip the router. Because the previous version stays warm, rollback is a second flip, not a redeploy.
# .github/workflows/blue-green.yml β cutover job (abridged)
name: Blue-Green Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Determine idle color
id: color
run: |
# Read which color currently serves live traffic from the router config
LIVE=$(kubectl get service app-router -o jsonpath='{.spec.selector.color}')
IDLE=$([ "$LIVE" = "blue" ] && echo green || echo blue)
echo "idle=$IDLE" >> "$GITHUB_OUTPUT"
- name: Deploy to idle environment
run: kubectl set image deploy/app-${{ steps.color.outputs.idle }} app=ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Smoke test idle before cutover
run: |
# Hit the idle environment directly, bypassing the live router
kubectl rollout status deploy/app-${{ steps.color.outputs.idle }} --timeout=120s
curl -sf "http://app-${{ steps.color.outputs.idle }}.internal/health" | jq -e '.status == "ok"'
- name: Flip the router (atomic cutover)
run: |
# A single patch redirects 100% of traffic β the old color stays warm for rollback
kubectl patch service app-router -p \
'{"spec":{"selector":{"color":"${{ steps.color.outputs.idle }}"}}}'Common mis-configurations:
- Deleting or scaling down the old environment immediately after cutover β you lose the instant rollback path. Keep it warm for at least one analysis window.
- Smoke-testing through the live router instead of hitting the idle environment directly β the test then validates the old version, not the new one.
- Forgetting database backward compatibility β the two versions share one database, so schema changes must be additive (expand-then-contract), or the flip breaks the version still serving traffic.
For static frontends, the same cutover is achieved with a symlink swap rather than a router patch β see atomic symlink deploys for static frontends.
Pattern 2 β Metric-Gated Canary (Intermediate)
When to use it: High-traffic services where even a brief exposure of a bad version to 100% of users is unacceptable, and you have enough traffic for statistically meaningful analysis within minutes.
A canary routes a small slice β typically 5% β to the new version, holds for an analysis window, and widens only if the new versionβs metrics match the stable baseline. The rollout is a step function guarded at every step.
# Argo Rollouts canary spec β the traffic ladder with automated analysis
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: app
spec:
strategy:
canary:
steps:
- setWeight: 5 # 5% of traffic to the new version
- pause: { duration: 5m } # analysis window
- analysis: # automated metric check β aborts on failure
templates:
- templateName: error-rate-and-latency
- setWeight: 25
- pause: { duration: 5m }
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100 # full promotionThe analysis step is the heart of the pattern: it queries your metrics backend and fails the rollout automatically if the new version regresses. See automating canary analysis with Prometheus metrics for the full AnalysisTemplate.
Common mis-configurations:
- Analysis windows too short to accumulate signal β a 30-second window on a low-traffic endpoint sees too few requests to detect a 1% error-rate regression. Size the window to guarantee a minimum request count, not a fixed clock time.
- Comparing the canary against absolute thresholds instead of the live baseline β a 0.5% error rate might be normal for your app; compare canary-vs-stable, not canary-vs-zero.
- Routing canary traffic by round-robin without session affinity β a user bouncing between versions on every request sees inconsistent behaviour.
Pattern 3 β Progressive Delivery with Release Control (Advanced / Production-Grade)
When to use it: Large teams shipping many changes per day, where deployment frequency must be decoupled from release risk and individual features need independent kill switches.
At scale, the infrastructure rollout and the feature release are separated entirely. Code ships to production continuously (often dark), and a feature flag governs exposure. This lets you deploy a canary of the binary while simultaneously running a percentage rollout of a feature inside it β two independent control loops.
// Release control lives in application code, not the pipeline.
// The deploy already happened; this decides who sees the feature.
import { flags } from "./flags";
export async function checkoutHandler(req: Request, user: User) {
// Deploy and release are independent: this code is live for everyone,
// but the new flow is gated to a growing cohort with an instant kill switch.
if (await flags.isEnabled("new-checkout-flow", { userId: user.id })) {
return newCheckout(req, user); // ramped 1% β 50% β 100% via the flag service
}
return legacyCheckout(req, user); // stable path β the automatic fallback
}Scale and failure modes at this level:
- Flag debt. Every temporary flag is a branch in production that must be removed after rollout. Track flag age and fail CI when a βtemporaryβ flag exceeds its TTL β otherwise the codebase accretes dead toggles that eventually cause an incident.
- Long-lived schema skew. With deploy and release decoupled, old and new code paths coexist for days. Database migrations must stay backward-compatible across the entire flag lifetime, not just the deploy window.
- Flag-service availability. The flag evaluation service becomes a production dependency. Cache flag state locally with a stale-but-available fallback so a flag-service outage defaults to the stable path rather than erroring.
Environment & Toolchain Matrix
| Scale tier | Strategy | Traffic control | Analysis | Rollback |
|---|---|---|---|---|
| Solo / small team | Atomic symlink or platform blue-green | Vercel/Netlify alias swap | Manual smoke test | Re-alias to previous deploy |
| Mid-size (single cluster) | Blue-green via Service selector | Kubernetes Service patch | Health check + basic metrics | Router flip to warm old env |
| High-traffic (multi-service) | Canary via Argo Rollouts / Flagger | Service mesh (Istio, Linkerd) or ingress weights | Prometheus AnalysisTemplate |
Automatic abort + traffic shift |
| Static / edge | Atomic manifest deploy | CDN percentage routing | Real-user monitoring (RUM) | Instant manifest revert |
| Large org / many teams | Progressive delivery + feature flags | Flag service (LaunchDarkly, Unleash, OpenFeature) | Per-feature metric cohorts | Flag kill switch (sub-second) |
Which tools you already run for pipeline architecture largely dictate this choice: teams on a service mesh get canary weighting for free, while teams on managed static hosting lean on atomic deploys and edge routing.
Cost & Performance Trade-offs
| Factor | Blue-green | Canary | Rolling |
|---|---|---|---|
| Peak infra cost during deploy | 2Γ (both environments full-size) | ~1.1Γ (small extra canary capacity) | 1Γ (in-place replacement) |
| Time to full rollout | Seconds (single flip) | 20β40 min (laddered with analysis) | 5β15 min (pod-by-pod) |
| Blast radius of a bad version | 100% at flip, but instantly reversible | β€5% until first gate passes | Grows with each replaced instance |
| Mean time to recovery | < 30 s (flip back) | 1β2 min (abort + shift) | Minutes (must redeploy old version) |
| Database coupling risk | High β both versions share one DB | High β same | Highest β many versions mid-roll |
| Best fit | Instant cutover, ample capacity | High traffic, low risk tolerance | Resource-constrained clusters |
The dominant cost lever is the duration of the doubled-capacity window in blue-green. Retire the old environment as soon as the analysis window closes β but not before β and use self-hosted runners to keep the deploy pipeline itself off billable minutes.
Failure Modes & Remediation
1. Cutover flips before the new environment is ready
Root cause: The router is patched before pods pass readiness, so traffic hits a version that is still starting. Symptom: a spike of connection-refused or 502 errors at the exact moment of cutover.
Fix: Gate the flip on kubectl rollout status completing and a direct health check succeeding against the idle environment, as in Pattern 1. Never flip on deploy-submitted; flip on ready-and-verified.
2. Canary looks healthy but the baseline is unhealthy
Root cause: The analysis compares the canary against a stable version that is itself degraded, so a regression passes the gate because βboth are equally bad.β
Fix: Add absolute floor guardrails alongside the relative comparison β e.g. abort if canary error rate exceeds 2% regardless of baseline. Relative comparison catches regressions; absolute floors catch shared outages.
3. Rollback fails because the old version was already scaled to zero
Root cause: A cost-optimization step scaled down the previous version immediately after cutover, so there is nothing warm to roll back to.
Fix: Keep the previous version at minimum viable replicas until the analysis window closes. Encode this as a policy in the pipeline, not a manual step. Full runbook in automated rollback triggers and runbook integration.
4. Schema migration breaks the version still serving traffic
Root cause: A destructive migration (dropped column, renamed table) ran during deploy while the old version β still live during blue-green validation β expected the old schema.
Fix: Use expand-then-contract migrations: add the new column, deploy code that writes both, backfill, switch reads, and only drop the old column in a later release once no running version references it.
5. Feature flag left on for a cohort after rollback
Root cause: Infrastructure rolled back but the feature flag stayed enabled, so the reverted binary still exposed a half-finished feature via a flag it no longer fully supported.
Fix: Treat the flag kill switch as part of the rollback runbook. Automated rollbacks that touch flagged features must flip the flag off as a first action, before or alongside the traffic shift.
Frequently Asked Questions
What is the difference between a deployment and a release?
A deployment moves new code onto production infrastructure; a release exposes that code to users. Traditional pipelines conflate them β deploying is releasing. Progressive delivery separates them: you deploy continuously (the binary is live but dark) and release deliberately by flipping a feature flag or shifting traffic weight. The separation shrinks blast radius because a deploy that goes wrong is invisible to users, and a release that goes wrong is reversed without a redeploy.
When should I choose blue-green over canary?
Choose blue-green when you need an instant, atomic cutover, can afford to run two full-size environments briefly, and value simplicity of reasoning. Choose canary when even a momentary 100% exposure of a bad version is unacceptable and you have enough traffic to analyse a 5% slice meaningfully within a few minutes. Many teams run both: blue-green for the frontend (cheap to duplicate) and canary for stateful backend services.
How fast should an automated rollback be?
Target a mean time to recovery under two minutes. With blue-green the old environment is already warm, so rollback is a routing flip that completes in seconds. With canary, rollback is aborting the rollout and shifting the traffic weight back to the stable version. The critical design rule is that rollback must be triggered by metric thresholds, not by a human noticing a graph β see automated rollback triggers.
Do progressive delivery strategies work for static frontends?
Yes. Atomic symlink deploys give static sites blue-green semantics β the new build is uploaded fully, then made live in one indivisible operation. Edge platforms support percentage-based canary routing at the CDN layer, and feature flags in the client bundle handle release control. The main difference is that βrollbackβ is re-pointing a symlink or manifest, which is faster than any server-side redeploy.
What metrics should gate a progressive rollout?
Gate on the golden signals: request error rate (HTTP 5xx plus application-level errors), latency at p95 and p99, and saturation (CPU, memory, queue depth, connection pool usage). Compare the new version against the live stable baseline over a fixed analysis window sized for a minimum request count. Abort on statistically significant regression, and add absolute-floor guardrails so a shared outage that degrades both versions equally still trips the brake.
Related
- Blue-Green Deployments for Full-Stack Apps β two-environment cutover mechanics, database compatibility, and warm-standby rollback.
- Canary Releases & Progressive Rollouts β laddered traffic weighting with automated metric analysis and abort conditions.
- Feature-Flag-Driven Release Management β decoupling deploy from release, cohort ramps, kill switches, and flag lifecycle hygiene.
- Automated Rollback Triggers & Runbook Integration β threshold configuration and incident-runbook wiring for hands-off recovery.
- Preview Environments & Environment Parity β validate deployment behaviour per-PR before it ever reaches a production rollout.