Progressive Delivery: Blue-Green, Canary, and Feature Flags Compared
← Back to blogDevOps

Progressive Delivery: Blue-Green, Canary, and Feature Flags Compared

J
Jason Miller
· 8 min read

Every team has a version of the same story: a deploy goes out, everything looks fine in the pipeline, and twenty minutes later the error rate graph goes vertical. The rollback takes five minutes. The damage was done in the first three.

The pipeline didn't fail — it did exactly what it was told. The problem is the deployment model: ship to 100% of traffic, then find out if it was a mistake. Progressive delivery exists to break that model. It separates deploying code (getting a new version running somewhere) from releasing it (letting it serve real traffic), and it lets you release in slices instead of all at once.

This post covers the three main techniques — blue-green, canary, and feature flags — what each one actually buys you, and where teams get the implementation wrong.

Deploy vs. release: the distinction that matters

In a traditional pipeline, deploy and release are the same event. kubectl apply finishes, the rollout controller cycles pods, and within a minute or two every user is hitting the new version. There's no window to observe behavior before it's everyone's problem.

Progressive delivery inserts a gap between those two events. The new version is running and reachable, but traffic exposure is controlled independently — by percentage, by cohort, or by an explicit toggle. That gap is where you catch problems while they're still small.

The three techniques below all exploit that gap, but they control exposure along different axes: infrastructure (blue-green), traffic percentage (canary), and code path (feature flags).

Blue-green: instant cutover, instant rollback

Blue-green runs two complete, independent environments — "blue" (current) and "green" (new). You deploy the new version entirely to green, run smoke tests against it directly, then flip a router or load balancer to send traffic to green. Blue stays warm and untouched, ready to take traffic back instantly if green misbehaves.

The appeal is simplicity: there's no partial-traffic state to reason about. Either everyone hits blue or everyone hits green. Rollback is a routing change, not a redeploy — typically seconds.

A minimal AWS setup using two target groups behind an ALB:

# GitHub Actions step — flip ALB listener to the green target group
- name: Deploy to green target group
  run: |
    aws ecs update-service \
      --cluster prod \
      --service api-green \
      --task-definition api:${{ github.sha }} \
      --desired-count 4

- name: Wait for green healthy
  run: |
    aws elbv2 wait target-in-service \
      --target-group-arn ${{ secrets.GREEN_TG_ARN }}

- name: Cut over listener to green
  run: |
    aws elbv2 modify-listener \
      --listener-arn ${{ secrets.LISTENER_ARN }} \
      --default-actions Type=forward,TargetGroupArn=${{ secrets.GREEN_TG_ARN }}

The cost is running double the infrastructure during the cutover window, and the fact that blue-green gives you an all-or-nothing bet. It catches "the new version doesn't start" and "the new version fails health checks" reliably. It does nothing for "the new version is subtly wrong for 2% of edge-case inputs" — that bug ships to 100% of traffic the instant you flip.

Canary: partial traffic, automated judgment

Canary releases route a small percentage of live traffic to the new version while the rest continues hitting the stable one, then ramp the percentage up as confidence builds. Where blue-green is binary, canary is a gradient — and that gradient only earns its complexity if something is actively watching it.

Argo Rollouts (a drop-in replacement for a standard Kubernetes Deployment) automates this with an explicit analysis step:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: api
spec:
  replicas: 10
  strategy:
    canary:
      steps:
        - setWeight: 10
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 50
        - pause: { duration: 10m }
        - setWeight: 100
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  metrics:
    - name: error-rate
      interval: 1m
      successCondition: result[0] < 0.01
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{job="api",status=~"5.."}[5m]))
            /
            sum(rate(http_requests_total{job="api"}[5m]))

If the error-rate query breaches the threshold during the pause, Argo Rollouts halts the rollout and rolls back automatically — no human needs to be staring at a dashboard at 2am. That automated judgment is the entire point of canary; without it you've just built a slower, more complicated version of "deploy and hope."

Canary catches problems blue-green can't: performance regressions, elevated error rates on a subset of real traffic, resource leaks that only show up under production load. What it doesn't give you is control over which users see the new version — that's traffic-based, not identity-based, so you can't canary a release to "beta users" or "enterprise accounts" specifically. For that, you need feature flags.

Feature flags: control at the code level

Feature flags decouple deployment from release entirely by pushing the decision into application code. The new code path ships to production inside a conditional, dormant until a flag service says otherwise. This means you can deploy on Tuesday and release on Thursday, target specific user segments, and kill a bad feature by flipping a boolean — no deploy, no rollback, no waiting on a pipeline.

Using the OpenFeature SDK (a vendor-neutral standard, backed by LaunchDarkly, Flagsmith, Unleash, and others) keeps you from hard-coupling to one provider:

import { OpenFeature } from "@openfeature/server-sdk";

const client = OpenFeature.getClient();

async function handleCheckout(req: Request) {
  const useNewPricingEngine = await client.getBooleanValue(
    "new-pricing-engine",
    false,
    { targetingKey: req.user.id, plan: req.user.plan }
  );

  return useNewPricingEngine
    ? newPricingEngine.calculate(req.cart)
    : legacyPricingEngine.calculate(req.cart);
}

Flags are the only technique that gives you targeting by identity — internal employees first, then a percentage of free-tier users, then everyone. They're also the only one that lets product and engineering decouple release timing entirely from deploy timing, which matters more than it sounds like once you have more than one team shipping to the same service.

The tradeoff is code complexity. Every live flag is a conditional someone has to reason about, test both sides of, and eventually remove. Flags that outlive their rollout become permanent forks in the codebase — more on that below.

Choosing between them

These aren't mutually exclusive, and mature teams usually run more than one:

  • Blue-green for infrastructure or platform-level changes where you need certainty and instant rollback — a new base image, a config change, a runtime version bump.
  • Canary for application code changes where you want automated, metric-driven confidence before full exposure — most day-to-day service deploys.
  • Feature flags for anything that needs identity-based targeting, staged rollout over days or weeks, or a kill switch independent of the deploy pipeline — new user-facing features, risky business logic changes.

A common production pattern: canary the deploy at the infrastructure level (catch crashes and error-rate regressions automatically) while the new feature itself sits behind a flag that's still off for everyone. The canary proves the build is safe; the flag controls when the feature actually reaches users.

Where teams get this wrong

Canary without automated analysis. Setting setWeight: 10 and then having an engineer eyeball a dashboard for five minutes isn't canary deployment — it's a manual step with extra YAML. If nothing is programmatically gating the promotion, you've added latency without adding safety.

Database migrations that don't fit the model. Blue-green and canary both assume both versions can run concurrently against the same data. A schema change that isn't backward-compatible breaks that assumption immediately — the old version breaks against the new schema, or vice versa. Progressive delivery for the app tier requires expand-contract migrations at the data tier; they're not optional extras, they're a prerequisite.

Flag debt. Flags are cheap to add and easy to forget. Six months in, teams routinely have dozens of flags nobody remembers the purpose of, each one a conditional branch that complicates every code review and testing pass. Treat every flag as having a removal date at creation time, and enforce it — a flag dashboard that flags (no pun intended) anything stale past 90 days is worth building early.

Alerting that isn't wired to the rollout. If your on-call alerts fire on the same thresholds regardless of whether a canary is in progress, you'll either miss a real regression buried in aggregate metrics, or get paged for noise that the canary process was already handling. Route rollout-phase metrics to the deployment tool, not just the general dashboard.

Wrap-up

The common thread across all three techniques is the same: don't bet the whole system on a change you haven't observed under real conditions. Blue-green gives you a fast, clean escape hatch. Canary gives you automated, data-driven confidence at the traffic layer. Feature flags give you precise control at the code layer, independent of deploy timing. Most mature delivery pipelines use some combination of all three, not because it's trendy, but because each one covers a failure mode the others don't.

We help engineering teams design and implement progressive delivery pipelines — from Argo Rollouts and canary analysis to feature flag architecture. Reach out if you're still shipping big-bang deploys and want a safer path forward.

Working on something similar?

We help engineering teams implement the practices covered in this post. First call is free.

Start a conversation →