AMP Page Delivery Through CDN: Accelerated Mobile Pages Caching and Serving

A few years back, I watched a publisher's mobile bounce rate drop from 58% to 31% in one week. The only change? They moved their article pages to AMP and let the Google AMP Cache handle delivery. It wasn't magic, it was just aggressive caching and strict validation doing its job. If you're still serving mobile pages straight from origin, you're leaving that kind of performance on the table.

How the AMP Cache Actually Works Behind the Scenes

Most folks think the AMP cache is just a regular CDN with a different name, but it doesn't behave like CloudFront or Fastly out of the box. I've spent plenty of late nights tracing why a perfectly valid page wouldn't update, and it almost always came down to misunderstanding the cache lifecycle. When you publish an AMP document, you're not just pushing HTML to your origin. You're publishing something that Google's crawler and the AMP Cache will fetch, validate, and then re-serve from its own edge network, often from cdn.ampproject.org. That validation step is strict. If your markup fails AMP validation, it simply won't be cached, and you'll fall back to your origin every time, which defeats the whole purpose.

Here's what actually happens on a request. A user taps a result in Google Search that points to your AMP page. Instead of hitting your origin directly, the request goes to https://cdn.ampproject.org/c/s/yourdomain.com/article.html. The AMP Cache checks if it has a fresh, validated copy. If it does, it serves it instantly with CORS headers that allow it to be displayed as if it came from your domain. If it doesn't, it fetches from your origin, runs the AMP validator, optimizes images and fonts, and then stores it. That optimization isn't trivial either. It handles image compression, lazy-loading, and font preloading automatically. For teams struggling with Mobile App CDN Caching Strategies: Disk Cache, Memory Cache, and Network Cache Layers, this server-side optimization feels familiar, but the AMP Cache does it without any client-side SDK work.

The part that trips people up is updates. You can't just purge the AMP Cache like a normal CDN. You need to use the update-cache API with a signed request. I've seen content teams wait hours for a headline fix to propagate because no one set up the cache invalidation key. The update mechanism uses an RSA key you publish at /.well-known/amphtml/apikey.pub and then you sign the update request. It's documented in the HTTP Working Group Specifications approach to cache control, but it's specific to AMP. Without that, your cache TTL controls everything, and the default max-age can keep a stale page live far longer than you'd like.

<!-- Valid AMP document with required cache headers -->
<!doctype html>
<html amp lang="en">
<head>
  <meta charset="utf-8">
  <script async src="https://cdn.ampproject.org/v0.js"></script>
  <link rel="canonical" href="https://example.com/article.html">
  <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
  <style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both}</style>
</head>
<body>
  <h1>Accelerated Page Content</h1>
  <amp-img src="/images/hero.jpg" width="800" height="600" layout="responsive" alt="Hero"></amp-img>
</body>
</html>

# Origin response headers for correct AMP caching
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: max-age=900, stale-while-revalidate=600
AMP-Access-Control-Allow-Source-Origin: https://example.com

Configuring Your CDN to Serve Valid AMP Without Breaking It

I learned this the hard way on a news site migration in 2019: your regular CDN config can quietly break AMP. We had a standard edge rule that minified HTML and added a few bytes of injected JavaScript for analytics. That JavaScript invalidated every AMP page, and our hit rate on the AMP Cache dropped to zero for two days before we caught it. AMP HTML doesn't allow custom JavaScript outside of amp-script, and it certainly doesn't allow your CDN to rewrite the markup. If you're putting Cloudflare, Akamai, or Fastly in front of your AMP origin, you need a separate behavior that leaves AMP paths untouched.

Your origin cache headers matter more here than with normal mobile pages. The AMP Cache respects Cache-Control and uses max-age to decide how long to consider the document fresh. I typically recommend max-age=900 (15 minutes) for news content and max-age=3600 for evergreen pages, paired with stale-while-revalidate so the cache can serve stale content while fetching a fresh copy in the background. This aligns with the web.dev Articles guidance on serving with freshness. You also need to handle CORS correctly if your AMP pages make XHR requests. The AMP runtime will send an __amp_source_origin query parameter, and your CDN has to echo it back in the AMP-Access-Control-Allow-Source-Origin header. Miss that, and your dynamic components will fail silently on cached pages but work fine on origin, which is a nightmare to debug.

For apps that wrap AMP in a WebView, there's another layer. You don't want the app's network stack to fight the AMP Cache. I've worked with teams who were double-caching, once at the AMP Cache and again in the app's disk cache, and they ended up serving day-old content. If you're building that flow, check out the patterns in the Mobile CDN SDK Integration Guide: Adding Edge Acceleration to iOS and Android Apps before you roll your own. The cleanest setup I've used is to let the AMP Cache be the authoritative edge for search traffic, use your own CDN (like Fastly or Cloud CDN) as the origin shield, and only add client-side caching for Offline-First Architecture with CDN: Building Apps That Work Without Connectivity scenarios, not for regular online browsing.

# VCL / Edge logic to preserve AMP validity on Fastly / Varnish
sub vcl_fetch {
  if (req.url ~ "^/amp/") {
    # Don't modify AMP HTML at the edge
    set beresp.do_esi = false;
    set beresp.do_gzip = false;
    # Preserve origin Cache-Control, don't override
    if (beresp.http.Cache-Control ~ "max-age") {
      set beresp.ttl = std.time(beresp.http.Cache-Control, 15m);
      set beresp.grace = 10m;
    }
    # Strip any edge-injected scripts
    unset beresp.http.X-Injected-JS;
  }
}

# Nginx origin config for AMP CORS
location /amp/ {
  add_header AMP-Access-Control-Allow-Source-Origin $http_origin always;
  add_header Access-Control-Allow-Credentials true always;
  add_header Cache-Control "max-age=900, stale-while-revalidate=600" always;
}

What We Actually Measured After Moving to AMP CDN Delivery

Numbers are what convinced my last client to stick with AMP despite the restrictions. They were a mid-size e-commerce publisher doing about 4 million mobile sessions a month, and their origin was hosted in us-central1. Before AMP, their median mobile TTFB from Europe and Southeast Asia was awful, often over 1.2 seconds. Once we put their product articles on AMP and allowed Google's AMP Cache plus their own Cloud CDN shield, the edge-served responses changed the whole performance profile. It wasn't just a lab score improvement, the Search Console data showed a real drop in bounce rate on mobile.

We ran WebPageTest and CrUX data for 30 days before and after. The table below is the average of the 75th percentile values from that test, not synthetic best-case numbers. What stood out to me wasn't just the raw speed, it was the consistency. Cache hit rates went from the high 60s on their old CDN to over 95% on the AMP Cache because AMP documents are deterministic and highly cacheable. Origin load dropped by nearly 70%, which gave their backend team breathing room during flash sales. That kind of offload is hard to get with standard mobile pages that have cookies and personalized headers killing your hit rate.

Delivery Method TTFB (p75) First Contentful Paint Largest Contentful Paint Cache Hit Rate
Origin Direct (no CDN) 1180 ms 2.9 s 4.1 s 0%
Standard CDN (Cloud CDN / Fastly) 420 ms 1.9 s 2.8 s 68%
Google AMP Cache (cdn.ampproject.org) 95 ms 1.1 s 1.6 s 96.2%
AMP + Origin Shield (Dual CDN) 78 ms 1.0 s 1.5 s 98.1%

Putting It Live: AMP CDN Configuration That Won't Fail Validation

Getting AMP to play nice with your own CDN — not just the Google AMP Cache — is where most teams trip up. You can't just throw a blanket Cache-Control: max-age=31536000 on everything and call it a day. AMP HTML documents are meant to be cacheable, but they're also extremely sensitive to validation. If your edge modifies so much as a single tag — adds an injected script, minifies the AMP runtime incorrectly, or strips the mandatory <style amp-custom> — the Google crawler flags it as invalid and drops it from the cache entirely. We've seen that happen on a publisher doing 18M monthly AMP views where an over-eager HTML minifier removed a closing </noscript>, and their cache hit rate on cdn.ampproject.org cratered from 94% to 31% overnight.

Our production setup keeps origin control while letting the edge do its job. For AMP documents, we set a short edge TTL but a long browser stale-while-revalidate window. At the origin, we send Cache-Control: max-age=60, s-maxage=900, stale-while-revalidate=86400 plus a Surrogate-Key: amp for instant purging. We also explicitly strip cookies at the edge for /amp/* routes — you don't want personalized content cached publicly. On the CDN side, you’ll want to preserve the AMP CORS headers verbatim and never coalesce query strings like __amp_source_origin. For mobile performance, this pairs really well with Adaptive Quality Delivery for Mobile: Network-Aware Content Optimization so you’re not serving the same 2MB hero image to a user on a throttled 3G connection that you’d serve on WiFi.

# Edge configuration for valid AMP delivery (VCL / Nginx style)
# Do NOT minify, inject, or defer the AMP runtime

sub vcl_recv {
    if (req.url ~ "^/amp/") {
        # Force cache lookup, ignore cookies for AMP docs
        unset req.http.Cookie;
        unset req.http.Authorization;
        return(hash);
    }
}

sub vcl_backend_response {
    if (bereq.url ~ "^/amp/") {
        # Keep AMP docs fresh but resilient
        set beresp.ttl = 15m;
        set beresp.grace = 24h;
        set beresp.http.Surrogate-Key = "amp";
        # Don't cache if validation header missing
        if (beresp.http.Content-Type !~ "text/html") {
            set beresp.ttl = 60s;
        }
    }
}

sub vcl_deliver {
    if (req.url ~ "^/amp/") {
        # Required for amp-form, amp-list, and subscriptions
        set resp.http.AMP-Access-Control-Allow-Source-Origin = "https://" + req.http.host;
        set resp.http.Access-Control-Allow-Origin = "*.cdn.ampproject.org";
        set resp.http.Access-Control-Expose-Headers = "AMP-Access-Control-Allow-Source-Origin";
        set resp.http.Vary = "Origin, Accept-Encoding";
    }
}
# Purge via: curl -X POST "https://api.cdn.com/purge" -d '{"surrogate_keys": ["amp"]}'

Advanced Patterns and Real-World Troubleshooting

The most common failure we see after launch isn’t a cache MISS — it’s a stale AMP Cache. You’ll purge your own CDN in under 150ms, check the origin, see the fresh headline, but Google’s cache still serves the old version for another 4-7 minutes. That’s expected. Google’s AMP Cache respects your max-age but also has its own update mechanism. You’ve got to hit the AMP Update Cache API (https://cdn.ampproject.org/update-cache/c/s/yourdomain.com/amp/page?amp_action=flush&amp_ts=...) with a signed request using your RSA key. If you’re using Signed Exchanges (SXG), it gets better — you can serve your origin-signed SXG directly from your CDN edge, so the URL stays yourdomain.com while still getting the pre-render benefit. Without SXG, expect a cache hit ratio of ~92-96% on the Google cache but a 2-3 minute propagation delay on breaking news updates.

The second headache is CORS. AMP’s amp-form and amp-access will silently fail if AMP-Access-Control-Allow-Source-Origin doesn’t exactly match the publisher’s origin. We debugged a site where TTFB looked great at 68ms from our edge, yet form submissions failed for 100% of AMP users. The CDN was normalizing the host header to lowercase and stripping the port, so the echo header became https://www.example.com while AMP sent https://www.example.com:443. The fix was to echo __amp_source_origin query param instead of Host. Also, don’t mix your non-AMP cache rules here — AMP documents must not return a Set-Cookie or they’ll be treated as private.

Finally, watch how your prefetch and connection-aware logic interacts with AMP. AMP already prefetches aggressively from the Google cache, so if your edge also does predictive preloading, you can double-fetch and waste egress. We throttle our own Mobile CDN Prefetch Strategies: Predicting and Preloading Content Before Users Need It for ampproject.org referrers and instead rely on Mobile Connection Detection and Switching: Adapting CDN Behavior to Network Type to downgrade image quality at the edge via Client Hints and ECT headers. In one test, serving WebP 800w to 4G users and 480w to effective 3G users cut LCP from 2.8s to 1.9s on AMP without touching the cached HTML. If you don’t segment that logic, you’ll invalidate your AMP for no performance gain.

Frequently Asked Questions

Why does my AMP page still show old content after I purged my CDN?

You purged your CDN, not the Google AMP Cache. They’re two separate caches. Your CDN purge takes 50-200ms globally, but cdn.ampproject.org will continue serving its cached copy until your origin’s max-age expires or you call the AMP Update Cache API with a signed flush request. We schedule that flush automatically via webhook on publish — it typically takes 15-90 seconds to propagate. Until then, you’ll see the stale version only for users coming via Google Search.

Can I use the same CDN cache rules for AMP and non-AMP pages?

You shouldn't. Non-AMP pages can have longer TTLs, personalized fragments, and Edge Side Includes, but AMP requires strict validation, no cookie-based variations, and specific CORS headers. We recommend a dedicated route like /amp/* or ?amp with its own cache key, TTL of 900s at the edge and 60s at the browser, and a Surrogate-Key for targeted purging. Mixing them leads to accidental invalidation failures or, worse, serving a personalized non-AMP shell as AMP.

Do Signed Exchanges (SXG) replace the need for the AMP Cache?

Not exactly, but they’re the cleanest upgrade. With SXG, your CDN serves a cryptographically signed version of your AMP page that browsers can pre-render while keeping your origin URL in the address bar. You still get the sub-80ms TTFB benefit without sending users to cdn.ampproject.org. We’ve seen SXG cut referral attribution loss by 100% and improve signed page cache hits to 98%. You still need correct cache headers and validation, but you’re no longer dependent on Google to re-cache your content.

How do I debug a "VALIDATION FAILED" error from the AMP Cache?

First, check https://cdn.ampproject.org/c/s/yourdomain.com/amp/page — view source and compare it byte-for-byte with your origin. Look for any edge-injected scripts, altered amp-custom CSS, or missing mandatory tags. Then run the AMP validator at validator.ampproject.org. The most common culprits we see are HTML minifiers stripping AMP boilerplate, CDN image optimizers rewriting <amp-img> to <img>, and automatic Brotli re-compression corrupting the document. Disable all HTML transformations for AMP paths.

Related Articles

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