BLOG/TEAM CI/CD
THEME
TEAM CI/CD

Blue/green deploys with instant rollback

Rollback shouldn't mean a frantic re-deploy of the last good build while the pager screams. Deploy to the idle slot, verify it before any traffic moves, flip atomically, and let a catch block undo it.

May 31, 2026 OrchStep Team 6 minROLE: SRESCALE: Any
RUNNABLE DEMO
Full source for this post: blog/blue-green-deploy
VIEW SOURCE

The worst time to discover your rollback plan is during the incident it was supposed to handle. The deploy went out, error rates spiked, and now rollback means re-running the pipeline with the previous tag — a fresh build, a fresh deploy, three to five minutes of bad traffic while you wait. "Rollback" that takes as long as a deploy isn't rollback. It's a second deploy you're doing under pressure.

Blue/green fixes this by never overwriting the running version. You keep two slots. The live one serves traffic; the idle one receives the new build. You verify the idle slot in isolation, then flip the router. Rollback is flipping it back — instant, because the previous version was never touched.

The discipline that makes it safe is sequencing: deploy to idle, verify before any traffic moves, then flip atomically — and have an automatic undo wired to the flip. That's exactly what a workflow with catch expresses.

Deploy idle, verify, flip, rollback on failure

Here's the deploy. Each step is a phase of the blue/green dance, and the flip step carries a catch that rolls back if the flip or its verification fails:

orchstep.yml
name: app
# Blue/green: deploy to the idle slot, verify it before any traffic moves,
# then flip atomically. If the flip or verify fails, catch rolls back —
# the live slot never saw a bad build.
defaults:
  version: "2.4.0"
  active: blue
  idle: green

tasks:
  # `orchstep run deploy --var version=2.5.0`
  deploy:
    steps:
      - name: release_idle
        func: shell
        do: echo "deploying {{ vars.version }} to idle slot {{ vars.idle }}"
      - name: verify_idle
        func: shell
        do: echo "verifying {{ vars.idle }} responds before any traffic"
      - name: flip
        func: shell
        do: echo "flipping traffic {{ vars.active }} -> {{ vars.idle }}"
        catch:
          - name: rollback
            func: shell
            do: echo "flip failed — traffic stays on {{ vars.active }}, {{ vars.idle }} drained"
        finally:
          - name: record
            func: shell
            do: echo "deploy of {{ vars.version }} finished"
      - name: confirm
        func: shell
        do: echo "{{ vars.idle }} is now live serving {{ vars.version }}"

The sequence is the safety. release_idle never touches the live slot. verify_idle runs against the new version while it's still dark — if it fails, no user ever saw it. Only after verification does flip move the router, and its catch block is the instant rollback: a failed flip leaves traffic exactly where it was. The finally records the attempt either way, so your audit log has the outcome regardless of branch.

orchstep run deploy --var version=2.5.0
# deploying 2.5.0 to idle slot green
# verifying green responds before any traffic
# flipping traffic blue -> green
# green is now live serving 2.5.0

Verify against the real health checks

The verify_idle step is where the demo's echo becomes your actual health probe — hit the idle slot's /healthz, check the version it reports, run a smoke test. Because verification happens before the flip, a bad build fails here, with zero blast radius:

curl -fsS https://green.internal/healthz | grep '"version":"2.5.0"'

If that check exits non-zero, the step fails before the flip ever runs. The live slot keeps serving the known-good version while you look at the logs. This is the difference between catching a bad deploy in a dark slot and catching it from the error-rate graph.

Rollback is a flip, not a deploy

The thing worth internalizing: in this model the previous version is still deployed and warm on the active slot the entire time. Rollback doesn't rebuild or redeploy anything — it points the router back. The catch block does it automatically on a failed flip; a manual rollback after the fact is the same one operation, run on purpose. Either way it's seconds, not a pipeline run. More on the catch/finally model: Error Handling.

What you actually gained

ConcernRe-deploy rollbackBlue/green + catch
Bad build reaches usersuntil you notice + redeploycaught in the dark idle slot
Rollback timea full deploy (minutes)a router flip (seconds)
Undo on a failed flipmanual, under pressureautomatic catch block
Verify before traffichope and monitorverify_idle gates the flip
Audit of the attemptscrape logsfinally records it

The honest caveat: blue/green wants two slots and a router you can flip atomically — load balancer target groups, a service mesh, DNS with a low TTL, whatever your platform gives you. If you're on a single box with no way to run two versions side by side, this pattern doesn't apply, and a fast forward-fix may serve you better. Where you do have two slots, wiring the flip with a catch turns rollback from a runbook into a code path.

Where to go next

Tired of rollback meaning "redeploy the old tag and pray"? Make it a flip with an automatic undo — curl -fsSL https://orchstep.dev/install.sh | sh.

#BLUE-GREEN#DEPLOYS#ROLLBACK#SRE
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — TEAM CI/CD