Back in 2019 I watched a retail client flip their DNS from one CDN to another at 2 AM on a Sunday, thinking a low TTL would make it clean. By 2:17 AM their origin was melting because the new provider's cache was cold, every edge node was thundering back to origin at once, and the old provider had already stopped serving. We spent the next six hours rolling back. That night is why I don't do hard cutovers anymore — running providers in parallel during a CDN transition isn't extra caution, it's how you keep your site up.
Why Hard Cutovers Break Down at Scale
On paper, a provider migration looks simple: lower your DNS TTL, point the CNAME at the new CDN, purge and pray. In practice, DNS doesn't work like a light switch. Even with a 30-second TTL, I've seen ISP resolvers and corporate DNS caches hold onto old records for 15-20 minutes, and mobile carriers are worse. During that window you've got users on both networks, but only one cache is warm. If you've ever looked at your origin logs during a cold cutover, you know the pattern — a vertical wall of 200s that immediately becomes a vertical wall of 503s as your origin connection pool exhausts.
Parallel running solves this by treating the migration as a traffic engineering problem, not a DNS event. For any serious multi-CDN migration, you need both providers serving live traffic simultaneously for days or weeks. We've learned to start with 5% to the new CDN, validate cache behavior, then ramp to 50/50 before we even think about decommissioning. It feels slower, but it's the only way you get real performance data under production load without risking everything. I walk teams through this phased approach in my Comprehensive CDN Migration Checklist: Every Step from Planning to Cutover because skipping the dual-serve phase is where most migrations quietly fail.
The other thing teams underestimate is cache state. Your old CDN has weeks of learned behavior — hot objects, tiered cache topology, custom cache keys. The new one knows nothing. If you don't run them together, you're not just testing the new provider, you're testing it under the worst possible condition: a completely cold cache. That's not a fair test, and it doesn't tell you how the platform will actually perform once it's warmed.
# Weighted DNS steering for parallel running with Route 53
# Gradually shift traffic without a hard cutover
resource "aws_route53_record" "www" {
zone_id = aws_route53_zone.primary.zone_id
name = "www.example.com"
type = "CNAME"
ttl = 60
weighted_routing_policy {
weight = 90 # Existing provider - start at 90%
}
records = ["existing-cdn.example.net"]
set_identifier = "cdn-primary"
}
resource "aws_route53_record" "www-new" {
zone_id = aws_route53_zone.primary.zone_id
name = "www.example.com"
type = "CNAME"
ttl = 60
weighted_routing_policy {
weight = 10 # New provider - validate with 10% live traffic
}
records = ["new-cdn.example.net"]
set_identifier = "cdn-secondary"
}
# Monitor both via separate health checks before increasing weight
# to 50/50 -> 10/90 -> 0/100 over 10-14 days
Building the Parallel Path: Routing, Origins, and Cache Consistency
Getting two CDNs to serve the same site without tripping over each other takes more coordination than most teams expect. The first thing I lock down is origin behavior. Both CDNs must see the same origin response for the same request, or your cache hit ratios and validation logic will diverge. That means normalizing cache keys, query string handling, and perhaps most importantly, Vary headers. I've seen migrations where the old provider was configured to ignore Vary: Accept-Encoding while the new one respected it, which doubled the cache footprint overnight and looked like a hit ratio regression that didn't exist. The HTTP Working Group Specifications are pretty clear on how Vary and cache validation should work, but every vendor implements the edge cases slightly differently.
You also need a single source of truth for routing decisions. Don't manage the split in two places. We typically put a DNS-based steering layer in front (Route 53, NS1, or even a small Anycast load balancer) and keep the logic there, rather than trying to have each CDN redirect to the other. And make your origins aware that two CDNs are coming. I always add a custom request header like X-CDN-Provider at the edge and log it at origin — it makes debugging cache poisoning or header mutation issues ten times faster. When you're evaluating how each vendor handles headers, shielding, and tiered caching, use a structured approach like the CDN Vendor Evaluation Matrix: Scoring Providers on What Actually Matters so you're comparing actual behavior, not sales decks.
// Edge worker to normalize behavior across both providers during parallel running
// Deploy identical logic to both CDNs to ensure cache-key parity
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(req) {
// 1. Normalize cache key - strip marketing params that shouldn't bust cache
let url = new URL(req.url);
['utm_source', 'utm_medium', 'gclid', 'fbclid'].forEach(p => url.searchParams.delete(p));
// 2. Tag origin requests so you can split logs and debug
let newReq = new Request(url, req);
newReq.headers.set('X-CDN-Provider', CDN_PROVIDER_ID); // 'fastly' or 'cloudfront'
newReq.headers.set('X-Migration-Phase', 'parallel-10pct');
// 3. Enforce consistent Accept-Encoding handling
// Don't let one CDN serve br and the other gzip for same key
if (!newReq.headers.get('Accept-Encoding')?.includes('br')) {
newReq.headers.set('Accept-Encoding', 'gzip');
}
return fetch(newReq, {
cf: { cacheTtl: 86400, cacheEverything: true }
});
}
What to Measure When Two CDNs Serve the Same Users
You can't run a parallel test if you can't compare the results. Before you shift even 5% of traffic, get unified logging and RUM in place. Each CDN's dashboard will tell you a slightly different story — they sample differently, they define "hit" differently, and their edge timing starts at a different point in the TLS handshake. We pipe both providers' logs into the same pipeline (usually via S3 + Athena or BigQuery) with a normalized schema that includes provider ID, PoP, cache status, and most critically, the client-side performance timing. Relying only on CDN-reported TTFB will mislead you; you need real user metrics as described in web.dev Articles to see the actual impact on LCP and INP.
During a recent CDN transition for a media site doing about 15M requests per day, we ran both providers at 50/50 for nine days. That was long enough to see weekday and weekend patterns, a bot spike, and one origin deployment. Here's what the side-by-side looked like once we normalized the data — and why we didn't cut over until day ten. If you're still writing your requirements doc, build these metrics into it now; the template in CDN RFP Template: Writing Requirements That Get Meaningful Vendor Responses has a section specifically for parallel-running SLAs and data access.
Don't just watch averages. I pay closest attention to p95 and p99 TTFB by geography, origin offload percentage, and error rate divergence. If one provider is 20ms faster on average but 300ms slower at p95 in Southeast Asia, that's not a win — that's a routing gap you'll hear about from customers.
| Metric (50/50 Parallel Period) | Existing CDN (Provider A) | New CDN (Provider B) | Analysis During Transition |
|---|---|---|---|
| Cache Hit Ratio (edge) | 94.2% | 91.8% (day 2) → 93.9% (day 9) | Provider B needed 5-6 days to warm tiered cache; day 2 number would have looked like a failure in a hard cutover |
| p95 TTFB (global, RUM) | 142ms | 118ms | 24ms improvement validated — consistent across NA/EU, 40ms better in APAC due to denser PoP map |
| Origin Requests / sec (peak) | ~820 req/s | ~1,450 req/s (day 1) → ~890 req/s (day 9) | Initial spike expected without shield warming; enabled origin shield in EU-CENTRAL-1 on day 3, cut origin load by 38% |
| 5xx Error Rate | 0.08% | 0.11% (days 1-3) → 0.06% (days 4-9) | Early errors traced to strict header validation on Provider B — fixed via edge worker normalization, not origin |
| Bandwidth Cost (per TB egress) | $42.00 | $38.50 | ~8.3% saving at scale, but only after commit tier; parallel running period cost was +12% due to duplicate cache fill |
Implementing Weighted Parallel Routing Without Tanking Hit Ratio
Most teams overcomplicate the routing layer. You don't need a bespoke global load balancer on day one. For 90% of migrations, weighted DNS plus sticky assignment is enough to run two CDNs in parallel safely. What we've learned running dual-provider setups for media and e-commerce workloads is that consistency matters more than granularity. If a user bounces between CDN A and CDN B on every request, you'll effectively halve your edge hit ratio on both platforms and hammer your origin. You need persistence.
Start by dropping your canonical hostname's TTL to 60 seconds at least 48 hours before you begin shifting traffic. Don't skip this — we've seen teams leave a 3600s TTL in place and then wonder why 35% of users are still pinned to the old CDN two hours after a weighting change. From there, use weighted records (Route 53, NS1, Dyn) or your authoritative DNS's traffic steering to split. Begin at 5% to the new provider, hold for 24-48 hours, then step to 10%, 25%, 50%. That slow ramp is how you catch origin shielding misconfigurations before they become incidents. Your CDN Proof of Concept Planning: Designing a PoC That Answers Real Questions should have already validated cache-key parity, but you'll still want to enforce it at the origin: normalize Accept-Encoding, sort query strings, and strip marketing params at the edge on both CDNs identically.
The trickiest part isn't DNS — it's keeping origin load flat while both caches are cold. Enable origin shield on both providers and, if possible, point them at the same shield POP. For one streaming customer, pointing both Fastly and Akamai shield to a single us-east-1 collector dropped origin egress from 18 Gbps to 6.4 Gbps during the 50/50 phase. And make sure your cache-control headers are authoritative from origin, not overlaid by CDN-specific rules that diverge. If one CDN respects stale-while-revalidate=86400 and the other doesn't, you'll see inconsistent hit ratios that look like performance regressions but are just config drift.
# NGINX / OpenResty edge split with sticky cookie for parallel CDN testing
# 5% canary to new CDN, persisted via cookie to preserve cache warmth
map $cookie_cdn_assignment $upstream_cdn {
default $split_assignment;
"cdn_legacy" "legacy.cdn.example.net";
"cdn_new" "new.cdn.example.net";
}
split_clients "${remote_addr}${http_user_agent}" $split_assignment {
5% "new.cdn.example.net";
* "legacy.cdn.example.net";
}
server {
listen 443 ssl;
location / {
# Set persistence cookie for 7 days if not already set
add_header Set-Cookie "cdn_assignment=$upstream_cdn; Path=/; Max-Age=604800; Secure; HttpOnly" always;
proxy_pass https://$upstream_cdn;
proxy_set_header Host $host;
proxy_set_header X-CDN-Shard $upstream_cdn;
# Normalize cache key - must match both CDN configs
proxy_set_header Accept-Encoding "gzip";
}
}
Advanced Patterns and Troubleshooting Dual-CDN Drift
Once you're past 25% traffic on the new CDN, the subtle issues surface. We've debugged the same three culprits repeatedly: header normalization, compression, and log reconciliation. If your legacy CDN automatically gzips on-the-fly and your new one requires an explicit Accept-Encoding forward, you'll see a 12-18% difference in cacheable bytes and a nasty origin CPU spike. Lock your Vary handling down. Log both CDNs' Cache-Status, Age, and X-Cache headers into your observability stack and alert if hit ratios diverge by more than 3-4% for the same URL cohort over a 15-minute window. That's your early warning for key mismatch.
Another pattern worth stealing is dark or shadow traffic. Instead of splitting end-users immediately, mirror 10-20% of requests to the new CDN without serving the response to the client. Cloudflare Workers, Fastly Compute, or a simple NGINX mirror directive can do this. You pay double the origin cost for a short window, but you get real-world cache warming and WAF validation with zero user impact. One SaaS platform we worked with ran shadow mode for 72 hours and caught a surrogate key purging bug that would have left 40k product images stale for 6 hours post-cutover. Pair this with your CDN Pilot Testing Methodology: Metrics, Duration, and Success Criteria so you're not guessing how long to shadow — we typically require 15k requests per URL pattern and a p95 TTFB within 10% of baseline before promoting shadow to live.
When things do drift, don't chase CDN dashboards — reconcile at the origin and at the client. Pull unified logs (we push both CDN logs to S3/GCS every 60 seconds and join on request_id) and compare origin fetch rate, 5xx ratio, and byte hit ratio side-by-side. If you see one CDN suddenly serving 8% more 304s, it's almost always a conditional request header being stripped. And if you're seeing intermittent TLS errors for 0.2% of users during DNS weighting changes, check your certificate SAN coverage and OCSP stapling — the new CDN may not be stapling yet. Have a single global kill switch ready: a DNS weight 100/0 or a config flag that disables the split in under 60 seconds. That rollback path is non-negotiable for any CDN Traffic Cutover Strategy: Migrating Without Downtime or Data Loss.
Frequently Asked Questions
How long should we run two CDNs in parallel?
Longer than you think, shorter than finance wants. For most enterprise sites, 2-4 weeks is the sweet spot. That covers a full cache warm-up cycle, a purge validation, and at least one traffic peak. Don't hard-cut after 2 days just because dashboards look green — we've seen stale-object bugs only surface on day 9 when the first long-TTL assets expired.
Will running dual CDNs double our origin costs?
Not if you configure shielding correctly, but expect a 15-30% bump during the 50/50 phase. Both caches are cold for unique URLs, so fetch rate temporarily rises. Point both shields at one origin shield region, keep TTLs identical, and avoid bypassing cache for A/B test cookies. For one retailer doing 800M requests/day, origin egress rose from 4.2 TB/day to 5.1 TB/day at peak parallel — well within budget.
Should we use DNS weighting or an application-layer split?
DNS weighting is simpler and works for 80% of migrations — it's provider-agnostic and fails open. App-layer splits (via Worker, edge function, or NGINX map) give you cookie stickiness and finer cohorts, like routing only iOS users or /api/* to the new CDN. If you need persistence or header-based routing, go app-layer. If you just need a clean 10% canary, stick with DNS.
How do we keep purges in sync across both CDNs?
Don't rely on manual dual purges. Your origin or CMS should fire purge API calls to both providers in parallel, with retries and idempotency keys. We wrap it in a small purging service that fans out to both CDNs and logs surrogate keys. Test this weekly during parallel — issue a purge for a single asset and verify both CDNs return MISS on the next fetch within 2-3 seconds. If one lags by 30+ seconds, you'll serve inconsistent content during deployments.