CDN Traffic Cutover Strategy: Migrating Without Downtime or Data Loss

Back in 2018 I watched a major retailer attempt a hard CDN switchover on Black Friday weekend — they just flipped the CNAME and hoped for the best. Within 90 seconds their origin was hammered with 40x normal load because the new CDN's cache was cold and every request was a miss. That night taught me that a successful traffic cutover isn't about DNS, it's about orchestrating state across two distributed systems that don't know about each other.

The Hidden Dependencies That Break a CDN Switchover

Most teams plan their CDN switchover around a DNS change and call it a day. That's the easy part. What actually breaks your live migration is everything the DNS record doesn't show: cache key normalization, origin shielding topology, header preservation, and stale content handling. I've seen migrations where the new CDN stripped query strings differently than the old one, so `/product?id=123&sort=price` and `/product?sort=price&id=123` suddenly became two separate cache objects. Hit ratio dropped from 94% to 61% in minutes and origin latency spiked.

Before you move a single byte of production traffic, you need to audit and mirror the behaviors you've spent years tuning. That means pulling your existing edge logic — VCL, Workers, or proprietary rules — and rebuilding it with byte-for-byte fidelity on the target. Don't assume standard headers behave the same. Cache-Control directives, Vary handling, and especially surrogate keys can vary wildly between providers. The HTTP Working Group Specifications are your source of truth here, not vendor docs. If you haven't mapped how both CDNs interpret `stale-while-revalidate` and `stale-if-error`, you're not ready for a zero downtime migration.

We always start with a parallel validation harness. This is non-negotiable. You want to replay real production requests against the new CDN without affecting users, compare responses, and catch normalization drift early. If you're early in planning, work through the Comprehensive CDN Migration Checklist: Every Step from Planning to Cutover first — it forces you to inventory certificate coverage, WAF rules, and image optimization settings that teams often forget until cutover night. I've seen a missing custom WAF rule allow a bot flood through during cutover that the old CDN had been silently blocking for months.

#!/bin/bash
# shadow-validate.sh - Replay production URLs against new CDN
# Uses Host override to test new CDN edge without DNS change

NEW_CDN_IP="203.0.113.45" # Anycast IP for new CDN validation edge
ORIGIN_HOST="www.example.com"
URL_LIST="top-10k-urls.txt" # From access logs, not sitemap

while read -r path; do
  # Request via NEW CDN
  new_resp=$(curl -s -o /tmp/new_body -w "%{http_code}:%{size_download}:%{time_starttransfer}" \
    --resolve "${ORIGIN_HOST}:443:${NEW_CDN_IP}" \
    -H "Host: ${ORIGIN_HOST}" \
    -H "X-CDN-Validate: shadow-test" \
    "https://${ORIGIN_HOST}${path}")

  # Request via OLD CDN (control)
  old_resp=$(curl -s -o /tmp/old_body -w "%{http_code}:%{size_download}" \
    "https://${ORIGIN_HOST}${path}")

  if [ "$new_resp" != "$old_resp" ]; then
    echo "MISMATCH ${path} | new:${new_resp} old:${old_resp}"
    diff -u /tmp/old_body /tmp/new_body | head -n 20
  fi
  # Check cache status and required headers
  curl -sI --resolve "${ORIGIN_HOST}:443:${NEW_CDN_IP}" \
    -H "Host: ${ORIGIN_HOST}" "https://${ORIGIN_HOST}${path}" \
    | grep -E "^(HTTP|X-Cache|Cache-Control|Vary|Age):"
done < "$URL_LIST"

Building a Dual-CDN Architecture for Live Migration

The only reliable way I've found to achieve a true zero downtime migration is to run both CDNs in parallel for at least one full TTL cycle plus cache warm-up time. That doesn't mean paying for two CDNs forever — it means architecting a 48 to 72 hour window where both edges can serve and your origin can handle the combined shield traffic. We set up an origin shield on the new provider that points to the same shield POP as the old one where possible, or we put a small NGINX layer in front of origin that coalesces requests with proxy_cache_lock. Without request coalescing, a cold cache on the new CDN will thunder-herd your origin.

DNS is where most engineers get nervous, but modern traffic management makes this far more controlled than a simple CNAME flip. Don't drop your TTL to 30 seconds the night before and hope resolvers honor it — many ISP resolvers don't. Instead, use weighted routing or a managed DNS traffic steering policy. We typically start with 1% of traffic on the new CDN via weighted CNAMEs or via our edge DNS provider's steering. This is also where your vendor selection pays off. If you haven't already scored your providers, use a structured CDN Vendor Evaluation Matrix: Scoring Providers on What Actually Matters to compare cache efficiency and shield footprint, not just PoP count. A vendor with fewer PoPs but better origin shielding can be safer during cutover than one with 300 edges and no collapsing.

You'll also need to handle TLS and certificates without blinking. If you're using Let's Encrypt or a shared SAN, provision and validate certificates on the new CDN at least a week early. For custom certificates, upload and test with a hosts-file override. And write your edge logic to be idempotent during the dual-serve period. Purges must go to both CDNs, and cache tags/surrogate keys should be normalized so a single purge API call invalidates both. I learned this the hard way when a content team purged a product page on the old CDN but the new CDN served stale pricing for 20 minutes to 10% of users during a canary.

// Edge function for consistent cache keys and dual-purge support
// Deploy on BOTH CDNs during migration window
export function normalizeCacheKey(request) {
  const url = new URL(request.url);
  // 1. Sort query params for consistent hashing
  const sortedParams = [...url.searchParams.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([k,v]) => `${k}=${v}`).join('&');
  
  // 2. Strip marketing params that shouldn't affect cache
  const paramsToStrip = ['utm_source','utm_medium','fbclid','gclid'];
  paramsToStrip.forEach(p => url.searchParams.delete(p));
  
  // 3. Normalize Accept-Encoding and Vary handling
  // per https://web.dev/articles/ guidance on content negotiation
  const acceptEnc = request.headers.get('Accept-Encoding') || '';
  const encoding = acceptEnc.includes('br') ? 'br' : 'gzip';
  
  return {
    cacheKey: `${url.pathname}?${sortedParams}::${encoding}`,
    surrogateKey: `product-${url.pathname.split('/')[2]}`,
    ttl: getTTLForPath(url.pathname) // e.g., 3600 for images, 60 for API
  };
}

async function handlePurge(surrogateKey) {
  // Purge BOTH providers in parallel during live migration
  await Promise.all([
    fetch(`https://api.new-cdn.com/purge/tag/${surrogateKey}`, { method: 'POST', headers: authNew }),
    fetch(`https://api.old-cdn.com/purge/tag/${surrogateKey}`, { method: 'POST', headers: authOld })
  ]);
}

Phased Traffic Shifting and Real-Time Validation Gates

Never go from 0% to 100% in one step, even if your shadow tests look clean. Real user traffic includes long-tail headers, malformed cookies, and bot patterns your top-10k URL list missed. Our standard traffic cutover ladder is 1% -> 10% -> 50% -> 100%, with at least 30 minutes of steady-state observation at each stage and automated rollback gates. Between each phase we check five signals: 5xx rate, cache hit ratio, p95 TTFB, origin request volume, and business metrics like checkout conversion. If any signal breaches its threshold, DNS weighting automatically fails back.

This is where you need observability that isn't vendor-biased. Don't rely solely on either CDN's dashboard — they sample differently and report hit ratio with different definitions (some count shield hits as hits). Ship both CDNs' edge logs to your own data store and compute metrics uniformly. We use real user monitoring (RUM) plus synthetic checks from at least six geographies. The web.dev Articles on Core Web Vitals are useful for setting TTFB and LCP budgets that you can enforce as cutover gates. And if you're still defining what "ready" means contractually, nail that down in your CDN RFP Template: Writing Requirements That Get Meaningful Vendor Responses so your vendor is obligated to support log streaming and instant purging during the migration window.

Keep both CDNs writable and purgeable for 48 hours after you hit 100%. DNS propagation isn't instant, and mobile apps with hardcoded DNS caching or corporate resolvers will hit the old CDN for days. Set your old CDN to serve stale on origin failure during this tail period, and don't decommission the configuration until edge logs show less than 0.1% of traffic still arriving there. That tail is also your emergency ripcord — if a major issue surfaces at 100%, you can shift weight back to the old CDN in under 60 seconds without a cold cache because it hasn't been evicted yet.

Cutover Phase Traffic on New CDN Target Hit Ratio / p95 TTFB Max 5xx Rate Origin RPS vs. Baseline
Canary 1% >88% / <180ms <0.15% +15% allowed (cache warming)
Early Ramp 10% >91% / <165ms <0.10% +8% allowed
Majority Shift 50% >93% / <150ms <0.08% +3% allowed
Full Cutover 100% >94% / <140ms <0.05% ±2% of baseline
Tail / Hold 100% (old on standby) >94% / <140ms <0.05% Old CDN <0.1% traffic

Automating the Cutover: Scripting Health Checks and Rollback Triggers

You don't want an engineer manually flipping a CNAME and staring at Grafana at 2 a.m. That's how you get a 20-minute outage you didn't see coming. A production-grade cutover is automated, incremental, and brutally objective about when to stop. We typically run weighted traffic shifting — either via DNS with low TTLs (30-60 seconds) or, better, via an edge load balancer / traffic steering service like NS1, Route 53, or Cloudflare Load Balancing. That lets you move 1% -> 5% -> 10% -> 25% -> 50% -> 100% with programmatic control instead of an all-or-nothing cut.

Every shift needs a validation gate that must pass before the next increment. Don't just watch for 5xx. We gate on four signals simultaneously over a 5-minute observation window: edge error rate delta < 0.05% compared to baseline, p95 TTFB within 15ms of the incumbent CDN, cache hit ratio within 2% of expected (if your PoC showed 92%, don't accept 84%), and origin request volume not spiking more than 15%. If any gate fails, the automation pauses. If it fails for two consecutive windows, it auto-rolls back. This is where a solid CDN Pilot Testing Methodology: Metrics, Duration, and Success Criteria pays off — you've already defined what "good" looks like during the pilot, so you're not inventing thresholds mid-migration.

The trick most teams miss is making rollback instantaneous and tested. Pre-stage the previous DNS weights or load balancer rules and keep the old CDN configuration warm and billable for at least 72 hours post-cutover. TTLs should be lowered to 30s at least 48 hours before you start (remember, the old TTL has to expire everywhere), and don't forget to plan your DNS layer itself — see CDN DNS Migration Planning: Changing DNS Providers Without Breaking Resolution if you're also moving authority. Here's a simplified health-check loop we use to automate the gates:

#!/usr/bin/env python3
import time, requests, sys

# Config from your CDN Proof of Concept
BASELINE_P95_MS = 145
ERROR_THRESHOLD = 0.0005  # 0.05%
HIT_RATIO_FLOOR = 0.90
CHECK_WINDOW_SEC = 300

def get_metrics(cdn="new"):
    # Pull from DataDog / Grafana / CDN analytics API
    resp = requests.get(f"https://metrics.internal/api/cdn/{cdn}/summary?window=5m").json()
    return resp  # { "p95_ms": 152, "error_rate": 0.0003, "hit_ratio": 0.91, "origin_rps": 840 }

def should_rollback(m):
    if m["error_rate"] > ERROR_THRESHOLD:
        return f"error_rate {m['error_rate']:.4f} > {ERROR_THRESHOLD}"
    if m["p95_ms"] > BASELINE_P95_MS + 15:
        return f"p95 {m['p95_ms']}ms exceeded baseline +15ms"
    if m["hit_ratio"] < HIT_RATIO_FLOOR:
        return f"hit_ratio {m['hit_ratio']:.2f} < {HIT_RATIO_FLOOR}"
    return None

# Example traffic shift loop: 5% -> 10% -> 25%
for weight in [5, 10, 25, 50, 100]:
    print(f"--> Shifting {weight}% to new CDN...")
    requests.post("https://api.traffic-steering.internal/shift", json={"new_cdn_weight": weight})
    time.sleep(CHECK_WINDOW_SEC)
    
    reason = should_rollback(get_metrics())
    if reason:
        print(f"GATE FAILED ({reason}). Rolling back to 0%...")
        requests.post("https://api.traffic-steering.internal/shift", json={"new_cdn_weight": 0})
        sys.exit(1)

print("Cutover complete. Hold old CDN warm for 72h.")

Advanced Patterns and Troubleshooting the Last 10%

The first 50% is usually smooth. The last 10% is where the weird stuff surfaces. The most common is the origin stampede. Your new CDN has a cold cache, and even at 25% traffic, if you haven't configured origin shielding or tiered caching correctly, you'll see origin RPS jump 300-400% for the first 15 minutes. We've seen origins that handled 800 RPS comfortably fall over at 2,500 RPS during a naive cutover. Fix it before you cut: enable a single origin shield region, turn on stale-while-revalidate and stale-if-error (e.g., 60s / 86400s), and consider a proactive cache warm. Don't purge the old CDN until you've confirmed the new one has built a stable working set — at least 2x your cache churn interval.

Next up: cache key mismatches. One CDN might normalize query strings alphabetically and lowercase headers by default, another might treat `?utm_source=x&id=123` and `?id=123&utm_source=x` as two different objects. That single difference can drop your hit ratio by 8-12 points overnight and look like a performance regression. The same goes for `Vary` handling and cookie whitelisting. Before any traffic moves, diff your cache-key configs line-by-line and run a log replay — take 1 million real access log lines and compare `X-Cache` results between CDNs. If you skipped a rigorous CDN Proof of Concept Planning: Designing a PoC That Answers Real Questions, this is where you'll pay for it.

Finally, don't get bitten by TLS and DNS edge cases. We've debugged cutovers that failed for 0.8% of users because of a missing CAA record for the new CDN's CA, or OCSP stapling wasn't enabled and caused 400ms TLS handshake spikes on Android 11 devices. If you're using DNSSEC, re-signing delays can make your new CDN's edge hostnames unresolvable for validating resolvers for up to an hour. Validate with `dig +dnssec`, `openssl s_client -status`, and real user monitoring (RUM) segmented by geography and ISP, not just synthetic checks from us-east-1. If error rates are flat but RUM shows a long tail in Brazil or India, you've got a regional PoP or peering issue — pause and investigate, don't push to 100%.

Frequently Asked Questions

How long should we keep both CDNs running in parallel?

At least 72 hours after you've hit 100% on the new CDN, but we recommend 7 days for high-traffic properties. That covers a full weekly traffic cycle and gives you time to catch cache hit ratio decay or billing log gaps. You're paying double for a week — think of it as cheap insurance. Keep the old config deployed but with 0% weight so rollback is a single API call, not a redeploy.

What TTL should we use during the migration?

Lower your steering record (e.g., `cdn.example.com` CNAME) to 30-60 seconds 48 hours before the first shift. Don't go to 5 seconds — many resolvers clamp low TTLs to 30s anyway and you'll just increase DNS query volume 10x without faster convergence. After you're stable at 100% for 24 hours, raise it back to 300-600 seconds to reduce lookup latency and improve resilience.

Should dynamic API traffic move at the same time as static assets?

No. Split them. Migrate static assets (images, JS, CSS) first — they're cacheable and lower risk. Then move your dynamic / uncacheable paths (`/api/*`, `/cart`) in a second phase with separate validation gates. Dynamic traffic is far more sensitive to latency variance (we gate p95 at +10ms, not +15ms) and origin concurrency, so isolating it makes rollback surgical.

Do we need to purge or warm the new CDN's cache after cutover?

Don't purge unless you have to — you want that cold cache to warm naturally. Do warm proactively if your working set is small and predictable: crawl your top 10k-50k URLs through the new CDN via the shield PoP 30 minutes before increasing weight. For sites with millions of long-tail objects, warming is ineffective; rely on origin shielding and a 2-4 hour gradual ramp instead.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker