Skip to content
All resources
IntermediateMigration18 min read

Zero-downtime cutover on AWS

The full rehearsal sequence we run before any production migration — traffic shadowing, dual writes, progressive reads, and the rollback switch we hope never to flip.

Last updated August 2026

Before you start

  • An AWS account with admin access to the target environment
  • Familiarity with CloudFormation or Terraform
  • A target database provisioned and reachable
  • Baseline latency and error-rate metrics for the current system
01

Why the window has to go

A retail estate that closes for four hours on a Sunday loses a Sunday. A payments platform that closes for four hours loses trust. The techniques below are not new — shadowing, dual writes, and progressive cutover have been documented for years — but they have moved from premium option to baseline expectation, and that changes the shape of the plan. Most of the effort happens before anything moves.

Rule of thumb: if the cutover hour is the interesting part of your plan, the plan is not finished.

02

Set up traffic shadowing

Mirror a copy of production traffic to the new stack and compare every response. Nothing is served from the new system yet — you are looking for disagreement. Route 53 weighted records or an ALB rule can fan the copy out; the important part is capturing both responses and diffing them structurally rather than byte for byte.

yamlcloudformation
ShadowTargetGroup:
  Type: AWS::ElasticLoadBalancingV2::TargetGroup
  Properties:
    Name: !Sub '${AppName}-shadow'
    Port: 8080
    Protocol: HTTP
    VpcId: !Ref VpcId
    HealthCheckPath: /healthz

ShadowRule:
  Type: AWS::ElasticLoadBalancingV2::ListenerRule
  Properties:
    ListenerArn: !Ref HttpsListener
    Priority: 10
    Actions:
      - Type: forward
        ForwardConfig:
          TargetGroups:
            - TargetGroupArn: !Ref LegacyTargetGroup
              Weight: 100
            - TargetGroupArn: !Ref ShadowTargetGroup
              Weight: 0        # mirrored, never served
03

Dual writes and reconciliation

Once reads agree, start writing to both systems. Run a reconciliation job nightly that compares row counts and checksums per table, and treat any non-zero delta as a blocking defect rather than a known issue. This is the stage teams are tempted to rush; it is also the stage that decides whether the cutover is boring.

bashreconciliation check
# nightly delta check — fails the pipeline on any drift
aws dms describe-table-statistics \
  --replication-task-arn "$TASK_ARN" \
  --query 'TableStatistics[?ValidationFailedRecords>`0`]' \
  --output table

DELTA=$(aws dms describe-table-statistics \
  --replication-task-arn "$TASK_ARN" \
  --query 'sum(TableStatistics[].ValidationFailedRecords)')

if [ "$DELTA" -ne 0 ]; then
  echo "Validation drift: $DELTA records — cutover blocked"
  exit 1
fi
04

Shift reads progressively

Move read traffic in slices — one percent, then five, then twenty-five — watching p99 latency and your error budget at each step. Hold each slice for at least one full traffic cycle, which for most businesses means a working day rather than an hour. If a slice degrades, shift back and diagnose before proceeding.

Automate the rollback trigger. A human watching a dashboard at 2am is not a control.

05

Cut writes, then hold

The write cutover is short — usually minutes — because everything is already in place. Afterwards, keep the old estate running and reverse replication armed for a full business cycle, including the month-end run if you have one. Only then decommission. The cost of a fortnight of duplicate infrastructure is trivial against the cost of discovering a month-end batch that only existed on the old system.

bashcutover
# 1. stop writes at the edge
aws elbv2 modify-rule --rule-arn "$MAINT_RULE" \
  --actions Type=fixed-response,FixedResponseConfig="{StatusCode=503}"

# 2. drain in-flight, confirm replication lag is zero
while [ "$(replication_lag_seconds)" -gt 0 ]; do sleep 2; done

# 3. promote target, flip weights
aws rds promote-read-replica --db-instance-identifier "$TARGET_DB"
aws elbv2 modify-rule --rule-arn "$SHADOW_RULE" \
  --actions Type=forward,TargetGroupArn="$NEW_TG"

# 4. reverse replication stays armed for 14 days

Was this useful?

Tell us what to write next.

Send feedback

Rather not do it yourself

We run this as
a scoped engagement.

Everything in this guide is something we deliver for clients — with the rehearsal, the rollback, and the handover documentation included.