Two years ago I watched a major e-commerce client knock their own site sideways for 47 minutes during what was supposed to be a simple origin migration. They'd built shiny new backend servers, pointed the CDN at them, and purged the entire edge cache to “start fresh.” Within seconds, that cold cache sent a thundering herd straight to the un-warmed origins, and we spent the next hour firefighting 503s instead of celebrating. You don't need a purge to switch origins — you need cache continuity.
Why a Full Purge Destroys Cache Continuity
Most teams assume that an origin migration has to involve cache invalidation. I've heard it a hundred times: “We’re moving to new servers, so the cache is stale anyway, right?” Not necessarily. If your URLs, cache keys, and response headers haven't changed, the object sitting at the edge is still perfectly valid no matter which backend built it. The edge doesn't care where the bytes came from last week; it cares whether the cached copy is still fresh according to its headers. When you wipe that cache, you're throwing away weeks of warmup and forcing your new origins to handle 100% of requests on day one.
That’s where backend moves get dangerous. A typical mid-size site I work with serves 85-90% of requests from edge cache. On a normal day their origins see maybe 800 requests per second. After a global purge during peak, that same cluster got hammered with 7,500 req/s in under a minute. Their autoscaling couldn't keep up, database connection pools saturated, and response times spiked from 180ms to over 4 seconds. The origin switchover itself took 30 seconds; the recovery took all afternoon. If you're planning a migration, your Comprehensive CDN Migration Checklist: Every Step from Planning to Cutover should treat cache preservation as a first-class requirement, not an afterthought.
The fix is to respect HTTP caching semantics through the switch. As defined in the HTTP Working Group Specifications, a cached response remains reusable as long as its freshness lifetime hasn't expired and its validators (ETag, Last-Modified) still match. If your new origin can serve byte-identical responses with identical Cache-Control, ETag, and Vary headers, there’s no reason to invalidate. I always tell teams to freeze cache-key logic and header generation for at least a week before migration. Don't change your CDN configuration and your origin infrastructure in the same window — you'll never know which change broke cache continuity.
# Preserve cache continuity - identical headers on old and new origin
# Verify before switching - don't change logic during migration
Cache-Control: public, max-age=86400, stale-while-revalidate=300, stale-if-error=86400
CDN-Cache-Control: max-age=604800
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Tue, 15 Jun 2026 12:00:00 GMT
Vary: Accept-Encoding, Accept
# Test both origins return identical validators:
curl -s -I https://old-origin.example.com/product/12345 | grep -E "ETag|Cache-Control"
curl -s -I https://new-origin.example.com/product/12345 | grep -E "ETag|Cache-Control"
# If these don't match exactly, fix it BEFORE you switch the CDN origin
Running Dual Origins for a Zero-Downtime Switchover
The pattern that's saved me the most pain is what I call a shadow origin setup. For about 10 days, we configure the CDN to know about both backends — the legacy origin and the new one — but we keep 99% of production traffic on the old one. The new origin isn't idle, though. We send it synthetic traffic, health checks, and a small percentage of real cache misses (usually 1-5% via weighted origin or a request header) so we can validate its behavior under real conditions without risking the whole site.
On Fastly, CloudFront, or Akamai this is straightforward. You define two origins in the same origin group and use weighted load balancing or a failover rule. I’ll typically start with a 95/5 split for 24 hours, monitor origin latency and 5xx rates on the new stack, then move to 50/50 for another day, then flip to 5/95 before finally making the new origin primary. The edge cache never gets purged — it just naturally refills from whichever origin served the miss, and since both origins return the same objects, clients never notice. For more on how we decide whether a provider can even support this kind of gradual shift, I point stakeholders to our CDN Vendor Evaluation Matrix: Scoring Providers on What Actually Matters — not every CDN handles origin weighting the same way.
What you're really testing during this phase isn't just connectivity. You're validating that the new backend's response fingerprint is identical. I once caught a backend move where the new Nginx config was stripping the Vary header on images. It seemed harmless, but it collapsed our cache key space and caused compressed and uncompressed variants to collide at the edge. We caught it because our 5% canary traffic showed a sudden 12% drop in hit ratio. That’s why you need request mirroring or at least detailed origin logging during the parallel run. The web.dev Articles on caching best practices put it well — consistent headers are more important than the server that sends them.
// Example: Fastly VCL for weighted origin switchover
// Keep cache intact - only origin fetch logic changes
backend old_origin {
.host = "origin-legacy.example.com";
.probe = { .window = 5; .threshold = 3; .interval = 5s; }
.weight = 95; // Start at 95, step down over days
}
backend new_origin {
.host = "origin-new.example.com";
.probe = { .window = 5; .threshold = 3; .interval = 5s; }
.weight = 5; // Start at 5, monitor errors closely
}
// Identical stale-while-revalidate behavior on both
sub vcl_fetch {
if (beresp.status == 200) {
set beresp.ttl = 1d;
set beresp.grace = 1d;
set beresp.stale_while_revalidate = 300s;
}
}
How to Prove Cache Continuity Before You Cut Over
You can't just hope the hit ratio holds — you've got to measure it. A week before any backend move, I snapshot a baseline: edge hit ratio, origin request rate, cache age distribution, byte hit ratio, and 5xx rate. Then I track those same metrics through the dual-origin phase. If cache continuity is working, you shouldn't see more than a 1-2 point dip in hit ratio when you shift weight to the new origin. If you see a 10-point drop, something's changed in your cache keys or headers, and you need to stop the migration.
My go-to dashboard pulls from both CDN logs and origin access logs. I compare the CDN's cache status (HIT, MISS, PASS, REVALIDATE) against the origin's 200 vs 304 responses. During a healthy origin migration, MISS and PASS should stay flat, HIT should stay above 85%, and origin 304s (revalidations) might bump up slightly as edge nodes revalidate stale objects against the new backend. That’s normal and much cheaper than a full miss. If your CDN RFP Template: Writing Requirements That Get Meaningful Vendor Responses didn't ask vendors about revalidation and stale-serving capabilities, now's the time you'll wish it had — those features are what keep a backend move invisible to users.
Don't rely on averages alone. Break it down by content type and geography. I've seen migrations where HTML hit ratio stayed perfect but API responses dropped from 65% to 12% because the new origin was sending Cache-Control: private on an endpoint that used to be public. Regional differences matter too — edge nodes in Singapore might have 7-day-old hot objects while a low-traffic PoP in South America is already fetching from the new origin. That table below is what I actually put in front of leadership before I approve the final origin switchover. If any metric is outside its threshold, we pause.
| Metric to Watch | Healthy Baseline (7d Avg) | During 50/50 Dual-Origin Run | Threshold Before Cutover |
|---|---|---|---|
| Edge Hit Ratio (all traffic) | 88.4% | 87.1% | > 86.0% (max -2.5pp drop) |
| Origin Requests per Second | 820 req/s | 845 req/s | < 1,100 req/s (+35% max) |
| Mean Cache Object Age | 2.8 days | 2.6 days | > 2.0 days |
| Origin 5xx Error Rate | 0.11% | 0.14% (new origin: 0.22%) | < 0.30% on new origin |
| P95 Origin Response Time | 187 ms | 192 ms (new origin: 201 ms) | < 250 ms on new origin |
Implementing the Origin Switch Without Invalidating Edge Cache
The trick to keeping your hit ratio intact isn't magic, it's making the new origin look identical to the old one from the edge's perspective. If your cache key includes anything that changes with the origin — Host header, a versioned path prefix, or a different Cache-Control you forgot to replicate — you'll fragment the cache and effectively purge yourself. We've seen migrations where a single extra Vary header dropped HIT ratio from 94% to 61% for 6 hours until objects naturally expired.
Before you flip DNS or origin pools, lock your cache key and validation headers. That means your new origin must return the exact same ETag generation logic (or Last-Modified), identical Cache-Control / Surrogate-Control, and consistent Content-Encoding. Don't let the new origin start gzipping differently or adding Cache-Control: private on what used to be public. Run both origins behind a single origin shield and pin your health checks to a 200 OK asset that also returns headers, not just a TCP check. If you're testing this properly, your CDN Proof of Concept Planning: Designing a PoC That Answers Real Questions should have already defined what "identical" means for your stack.
In practice, we implement this as a weighted origin group with header normalization at the edge. You keep 100% of traffic on Origin A, mirror 5-10% of MISS traffic to Origin B for a soak test, and compare validation. Once byte-for-byte headers match for 24-48 hours, you shift weight to 50/50, then 100% to the new origin — all without a single purge. Your CDN Pilot Testing Methodology: Metrics, Duration, and Success Criteria should be tracking HIT ratio, origin latency p95, and revalidation rate (304 vs 200) during this window, not just availability.
# Fastly VCL-style origin switching without cache invalidation
# Keep cache key stable - don't use origin hostname in hash
sub vcl_recv {
# Normalize host for cache key - both origins serve same content
set req.http.X-Orig-Host = req.http.Host;
set req.http.Host = "cdn.example.com";
# Weighted origin selection - start 95/5 canary
if (randombool(5, 100)) {
set req.backend = F_new_origin;
set req.http.X-Origin-Canary = "new";
} else {
set req.backend = F_legacy_origin;
}
}
sub vcl_fetch {
# Enforce consistent TTL - ignore accidental no-cache from new origin
if (beresp.http.Cache-Control ~ "private|no-store") {
# Log and override - don't let misconfig poison edge
set beresp.ttl = 1h;
set beresp.http.X-Cache-Warning = "overridden-private";
}
# Preserve ETag/Last-Modified exactly as origin sent it
# Do NOT strip - you'll break 304 revalidation continuity
}
sub vcl_hit {
# Serve stale on origin failure during cutover
if (obj.ttl >= 0s) {
return (deliver);
}
if (std.healthy(req.backend)) {
if (obj.ttl + obj.grace > 0s) {
return (deliver);
}
}
}
Troubleshooting Cache Drift, Shield Mismatches, and Revalidation Storms
Even with perfect config, you'll hit drift. The most common failure we see post-switchover is a revalidation storm. If your new origin generates ETags differently — say, adding a weak W/ prefix or using a different hashing algorithm — every conditional request (If-None-Match) that should have been a cheap 304 turns into a full 200 miss. One client saw origin egress spike 8x for 3 hours after cutover because Nginx etag off was set on the new pool. Always diff raw origin responses: curl -I -H "If-None-Match: ..." against both origins before and after the switch.
Second culprit: shield misconfiguration. If you're using origin shield (and you should), that shield POP becomes your system of record. If your shield still points to the old origin while edges point to the new one, you'll get fragmented tiers and thundering herd on shield MISS. During migration, pin the shield to the new origin first, let it warm for 30-45 minutes, then flip the edge. And watch your collapse-forwarding / request coalescing settings — if they're disabled, 1,000 simultaneous MISSes for the same uncached object will hammer your new origin and trip autoscaling.
If your HIT ratio does drop more than 3-4% post-cutover, don't panic-purge. Check three things in order: 1) Vary header cardinality, 2) Cache-Control divergence on 10-15 sample URLs, and 3) Whether you've changed query-string sorting or stripping. We've recovered 12-15 points of hit ratio just by reverting a new Vary: Accept-Encoding, Accept-Language that wasn't there before. For a controlled rollback plan, refer to your CDN Traffic Cutover Strategy: Migrating Without Downtime or Data Loss — you should be able to shift origin weight back to 100% legacy in under 60 seconds without touching the edge cache.
Frequently Asked Questions
Do I need to update the cache key when I change origin domains?
No — and you shouldn't. Keep the cache key origin-agnostic. Use a normalized Host and don't include the origin hostname or IP in the hash. If you change the key, every object becomes a MISS and you've just done a soft purge. The only thing that should change is the backend fetch address, not how the object is stored.
How long should I run dual origins before fully cutting over?
At least 24 hours, ideally 72 hours if your TTLs are long. You want to cover one full TTL cycle plus peak traffic. We typically run a 5% canary for 24 hours, then 50/50 for 12-24 hours while monitoring 304 revalidation rates and origin p95 latency. If your longest TTL is 7 days, you won't get full overlap, but you can still validate continuity on your top 1,000 hot objects which usually drive 80%+ of HITs.
Should I use stale-while-revalidate during the migration?
Absolutely. Set stale-while-revalidate=60 and stale-if-error=86400 at the edge before you start. It gives you a safety net — if the new origin returns 500s or bad headers for 30-45 seconds during cutover, edges will serve stale for up to 24 hours instead of queuing MISSes. It's the difference between a blip and a full outage.
Why am I seeing 200s instead of 304s after switching origins?
99% of the time it's an ETag or Last-Modified mismatch. Compare headers byte-for-byte between origins. Even a tiny difference — quotes around the ETag, or a one-second drift in Last-Modified — breaks conditional requests. Also check that your new origin actually honors If-None-Match and If-Modified-Since and isn't configured to always return 200.