CDN & Edge Media Delivery

Pushing image transformation out to the edge changes the economics of media delivery. Instead of pre-generating every format and every width at build time and shipping the whole matrix to your origin, you store one high-quality master and let the edge derive AVIF, WebP, and the exact pixel dimensions each viewport needs — cached the first time and served from memory forever after. Done well, this collapses origin egress, shortens the critical path to the Largest Contentful Paint image, and lets a single URL serve the right bytes to a Safari 14 phone and a Chrome 121 desktop at the same time. Done badly, it poisons caches, doubles origin fetches, and serves an undecodable AVIF to the exact clients least able to recover. This section covers the theory and the platform-specific mechanics that separate the two outcomes.

The hard problems at the edge are not “how do I resize a JPEG.” They are: how does a shared cache decide which stored variant answers a given request, how do you keep that decision from fragmenting your hit rate into uselessness, and how do you insulate your origin from the transformation load when a cold cache stampedes it. Every platform below solves those three problems differently, and the differences are exactly where production incidents come from.


What this section covers

The topics below build from the general negotiation model down to per-platform configuration and the monitoring that keeps a working setup from silently regressing. Each area has its own dedicated pages.

Cloudflare Image Resizing and Polish — the /cdn-cgi/image/ transformation URL, Workers-based cf.image resizing, and Polish’s zero-config automatic WebP/AVIF re-encoding. When each applies, how they bill, and how to debug what actually reached the browser.

AWS CloudFront Cache Behaviors for Media — cache policies that include Accept in the cache key, CloudFront Functions versus Lambda@Edge for format routing, and diagnosing the cache misses that quietly send every request to your origin.

Fastly VCL for Image Format Negotiation — normalizing req.http.Accept in vcl_recv to a small set of cache variants, driving the Fastly Image Optimizer, and using shielding to concentrate transformation work on a single POP.

Monitoring & Regression for Media Delivery — enforcing image-weight budgets in Lighthouse CI, diffing WebPageTest filmstrips for LCP regressions, and tracking p75 LCP field data through the CrUX API so a bad deploy shows up before your users report it.


Edge delivery overview

The diagram traces one image request from the master asset on your origin through the edge transformation and cache layer to the browser. The cache-key box is the pivot point: everything upstream produces bytes, everything downstream consumes them, and the key decides whether a request is answered from edge memory or forwarded to origin.

Edge media delivery flow A left-to-right flow: Origin master image feeds an Edge Transform stage (decode, resize, re-encode to AVIF or WebP based on Accept). The transform output is stored under a Cache Key derived from the URL plus normalized Accept. On a hit the key returns cached bytes; on a miss it re-runs the transform. The final stage delivers to the Browser with Content-Type and Vary headers. Origin master one high-quality JPEG / PNG source shielded POP fetch Edge transform decode master resize to width/dpr read Accept header re-encode AVIF/WebP set Content-Type store Cache key URL + options + normalized Accept → avif / webp / jpeg HIT: serve memory MISS: re-transform miss re-runs transform serve Browser decodes format Vary: Accept paints LCP ↑ cold cache stampede hits origin ↑ raw Accept in key = cache shattered into hundreds of variants ↑ missing Vary = wrong format cached downstream

Core theory: transforming and negotiating at the edge

Edge image transformation

Edge transformation moves the decode-resize-re-encode pipeline from your build step (or origin server) into the CDN’s compute layer, executed the first time a given variant is requested. A request for a 640-pixel-wide AVIF derived from a 4000-pixel master causes the edge to fetch the master once, run it through a resize and an AVIF encoder, cache the result, and return it. Every later request for that same variant is a pure cache read.

This inverts the classic build-time model. With build-time generation you decide up front which widths and formats exist; anything you did not pre-generate 404s or serves an oversized fallback. With edge transformation the set of variants is open-ended — any width in the option string is valid — but you pay a one-time encode cost per unique variant and you must bound that set deliberately, because an unbounded set (arbitrary width values from client-side JavaScript, for example) turns every request into a cache miss and a fresh encode.

Tradeoff: on-the-fly AVIF is CPU-expensive. A single 1600-pixel AVIF encode is tens to low hundreds of milliseconds of edge compute. That cost is invisible on a warm cache and brutal on a cold one. The mitigation is a constrained, enumerable variant set plus origin shielding, both covered below.

Accept-based negotiation at the edge

Browsers advertise decodable image formats in the Accept request header. Chrome 121 sends image/avif,image/webp,image/apng,*/*;q=0.8; Safari 14 sends image/webp,image/*,*/* (no AVIF); an ancient client sends only */*. Edge negotiation reads this header and picks the best format the client can actually decode — AVIF if present, else WebP, else the JPEG master — from a single canonical URL. The browser never has to author a <picture> fallback chain for format, because the edge already served the right bytes.

This is strictly more robust than static <picture> negotiation for format selection, because the decision is made against the live Accept header rather than a hard-coded type gate. It does not replace <picture>/srcset for resolution selection — the browser still chooses which width to request based on sizes and the display — but it removes the need to physically store an AVIF and a WebP copy of every asset. The format axis collapses into one URL. This is the same negotiation contract described for origin servers in Cache-Control Headers for Image and Video Assets, pushed one hop closer to the user.

Cache-key design

A cache is a map from key to bytes. For negotiated media the key must encode everything that changes the response: the base URL, the transformation options (width, quality, fit), and enough of the Accept header to distinguish AVIF-capable clients from WebP-only ones — but no more than that. The design failure that fragments hit rates is keying on the raw Accept string. Chrome, Firefox, Safari, and every crawler send subtly different Accept values (different q weights, different */* orderings), so a raw-Accept key produces a distinct cache entry per user-agent family for the exact same image. Hit rate collapses; origin load and encode cost explode.

The fix is normalization: collapse the infinite space of real Accept strings into a tiny enumerated set — avif, webp, or jpeg — and key on that token instead. Fastly does this with a req.http.Accept rewrite in vcl_recv; Cloudflare does it internally for Polish and Image Resizing; CloudFront does it with a cache policy that either includes Accept (and relies on you normalizing upstream) or, more robustly, with a function that maps Accept to a small custom header used in the key.

Vary: Accept versus a normalized cache key

There are two correct ways to make a shared cache serve the right format, and mixing them up is a classic incident:

  • Vary: Accept tells every downstream RFC-9111 cache (the CDN, corporate proxies, the browser cache) to store a separate object per distinct Accept value. It is honest and standards-compliant, but because Accept values vary wildly between clients, a literal Vary: Accept can itself shatter the cache unless the CDN normalizes Accept before applying it.
  • A normalized cache key keeps the key internal to the CDN: the edge computes avif|webp|jpeg from Accept, keys on that, and typically emits a stable representation to downstream caches. This is what Cloudflare does automatically and what a well-written Fastly VCL does explicitly.

Warning: emitting Vary: Accept while also letting a downstream CDN key on the raw header double-fragments the cache — once for the header value and once for the internal token. Pick one model per hop. The rule of thumb: normalize Accept to a token as early as possible, key on the token, and only emit Vary: Accept to caches you do not control (the browser and intermediary proxies) so they at least do not serve an AVIF to a WebP-only client.

Shielding and origin offload

A CDN has dozens or hundreds of edge POPs, each with its own cache. On a cold deploy, a popular image can miss simultaneously at every POP, and every POP independently fetches the master from your origin — a thundering herd that can saturate origin bandwidth exactly when traffic is highest. Shielding (Fastly’s term; Cloudflare calls the analogous mechanic Tiered Cache / Argo, CloudFront calls it Origin Shield) designates one intermediate POP that all other POPs consult before reaching origin. Origin then sees at most one fetch per variant instead of one per POP. For edge transformation this matters doubly: the shield POP also concentrates the expensive encode, so a variant is transformed once globally rather than once per region.


Platform capability reference

The table compares how the three major CDNs handle the four capabilities that decide a media architecture. “Edge compute” is the mechanism you would use for custom routing logic beyond the built-in transformer.

Capability Cloudflare Fastly AWS CloudFront
On-the-fly AVIF/WebP Yes — Image Resizing (/cdn-cgi/image/) and Polish auto re-encode Yes — Image Optimizer (?format=… / auto) No native transformer; build with Lambda@Edge + Sharp
Automatic format from Accept Yes — Polish webp/avif, format=auto in Image Resizing Yes — format=auto with normalized Accept in VCL Manual — function maps Accept, cache policy keys on it
Vary: Accept handling Respected automatically; Polish keys internally Honors Vary; typically normalize Accept in vcl_recv first Must add Accept to the cache policy or it is stripped
Edge compute for custom logic Workers (fetch with cf.image options) VCL, plus Compute (Wasm) CloudFront Functions (lightweight) / Lambda@Edge (heavy)
Purge granularity Single-file, tag, or wildcard purge Instant URL and surrogate-key purge (~150 ms) Path-based invalidation (slower, quota-limited)
Where transform runs Edge, cached per variant Edge / shield POP Lambda@Edge region, cached per variant

Tradeoff: Cloudflare and Fastly ship a managed transformer, so you write configuration; CloudFront makes you assemble the transformer from Lambda@Edge, so you write and maintain code. The CloudFront path is more work but gives byte-level control over the encoder and its parameters — see Lambda@Edge AVIF conversion on CloudFront.


Canonical delivery pattern

The most portable canonical pattern is a transformation URL that carries the derivation intent in the path and lets the edge negotiate format from Accept. Cloudflare’s form is the clearest example:

<!--
  The /cdn-cgi/image/ prefix is intercepted by Cloudflare's edge before it
  ever reaches your origin. The options segment declares the derivation:
    width=640     target width in device pixels (bounds the variant set)
    quality=75    perceptual quality; 75 is a strong photographic default
    format=auto   negotiate AVIF/WebP/JPEG from the request Accept header
  The final segment is the ORIGIN path of the master image. One tag, and
  the edge serves AVIF to Chrome and WebP to Safari 14 from this single URL.
-->
<img
  src="/cdn-cgi/image/width=640,quality=75,format=auto/img/hero-master.jpg"
  srcset="/cdn-cgi/image/width=640,quality=75,format=auto/img/hero-master.jpg 640w,
          /cdn-cgi/image/width=1280,quality=75,format=auto/img/hero-master.jpg 1280w"
  sizes="(max-width: 700px) 100vw, 640px"
  width="640" height="400"
  alt="Product hero photographed on a neutral background"
  fetchpriority="high">

For CloudFront the canonical unit is not a URL prefix but a cache policy that admits Accept into the key so the negotiated variant is stored correctly:

// CloudFront cache policy (Terraform-style shape). Without Accept in the
// cache key, CloudFront strips it and caches whichever format was produced
// first — then serves that one format to every client. This is the single
// most common cause of "AVIF shows up in Safari 14" incidents on CloudFront.
{
  "Name": "media-accept-negotiation",
  "ParametersInCacheKeyAndForwardedToOrigin": {
    "HeadersConfig": {
      "HeaderBehavior": "whitelist",
      "Headers": { "Items": ["Accept"] } // normalize upstream to avoid fragmentation
    },
    "QueryStringsConfig": { "QueryStringBehavior": "whitelist",
      "QueryStrings": { "Items": ["width", "quality"] } },
    "CookiesConfig": { "CookieBehavior": "none" }
  }
}

Full walk-throughs live in CloudFront cache policy for Vary/Accept negotiation and, for the URL-parameter form, Configuring Cloudflare Image Resizing URL parameters.


Pipeline integration

Fastly: normalize then negotiate

Fastly runs your logic in VCL. The pattern is to collapse Accept to a token in vcl_recv so the cache keys on three values, not thousands:

sub vcl_recv {
  # Normalize the wildly-varying Accept header down to a single token.
  # This token — not the raw header — becomes part of the cache variant,
  # so Chrome, Firefox, and crawlers that all support AVIF share ONE entry.
  if (req.http.Accept ~ "image/avif") {
    set req.http.X-Format = "avif";
  } elsif (req.http.Accept ~ "image/webp") {
    set req.http.X-Format = "webp";
  } else {
    set req.http.X-Format = "jpeg";
  }
  # Shielding: funnel all POPs through one datacenter so origin and the
  # image transform each run once per variant globally, not once per POP.
  if (req.http.host && !req.http.Fastly-FF) {
    set req.backend = ssl_shield_bwi_va_us;
  }
}

The X-Format token then drives the Image Optimizer format and is included in the object variant. Details: Fastly VCL: normalize Accept header for AVIF.

Cloudflare Workers: programmatic resizing

When the built-in URL form is not expressive enough — conditional quality, signed URLs, per-route rules — a Worker calls fetch with cf.image options:

export default {
  async fetch(request) {
    const accept = request.headers.get('Accept') || '';
    // Choose the encoder format from the live Accept header. format:'auto'
    // also works; picking explicitly lets you gate AVIF behind a flag.
    const format = accept.includes('image/avif') ? 'avif'
                 : accept.includes('image/webp') ? 'webp'
                 : 'jpeg';
    const url = new URL(request.url);
    return fetch('https://origin.example.com' + url.pathname, {
      cf: {
        image: {
          width: 640,        // bound the variant set — never pass raw client input unclamped
          quality: 75,       // perceptual quality target
          format,            // negotiated above
          fit: 'scale-down', // never upscale past the master's intrinsic size
        },
        // Cache the transformed result at the edge for a year; the master is
        // content-hashed so a new deploy produces a new URL rather than a stale hit.
        cacheEverything: true,
        cacheTtl: 31536000,
      },
    });
  },
};

The full Workers and Polish setup is in Cloudflare Image Resizing and Polish.

Monitoring the pipeline

An edge media setup degrades silently: a cache policy change drops the hit rate, a quality bump inflates payloads, a format regression ships AVIF to WebP-only clients. Wire the pipeline to a budget so regressions fail a build rather than a user’s LCP — enforce image-weight assertions in Lighthouse CI budget enforcement for image weight and watch the p75 field trend in tracking LCP field data with the CrUX API.


Tradeoffs & failure modes

Failure mode Cause Fix
Hit rate collapses after enabling negotiation Cache keyed on raw Accept header, one entry per user-agent family Normalize Accept to avif/webp/jpeg before it enters the key
Safari 14 shows a broken image Edge served AVIF; Accept not consulted or wrong format cached Read Accept at the edge; on CloudFront add Accept to the cache policy
Origin bandwidth spikes on every deploy Cold caches at every POP fetch the master simultaneously Enable shielding / Origin Shield / Tiered Cache to collapse origin fetches
Edge CPU / bill spikes Unbounded width values create a new encode per request Clamp width to an allowlist of breakpoints; reject arbitrary values
Transformed image ignores a new master Long immutable TTL on a non-hashed URL Content-hash the master path so a new asset is a new cache key
format=auto still serves JPEG to Chrome Accept stripped by an intermediate proxy or not forwarded to the transformer Confirm Accept reaches the edge; forward it in the cache/origin request policy
Double cache fragmentation Emitting Vary: Accept while the CDN also keys on raw Accept Normalize once; key on the token; reserve Vary for caches you do not control
Purge does not clear a bad variant Purged the master path but not the derived transformation URLs Purge by surrogate/cache tag, or purge the /cdn-cgi/image/… derivations too

Browser & CDN compatibility matrix

Format decode support is a browser property; the edge only decides which of these you send. The columns match the site-wide baseline.

Feature Safari 14 Safari 16 Chrome 85+ Firefox 93+ Edge 18+
Accepts image/webp Yes Yes Yes Yes Yes
Accepts image/avif No Yes (16.0+) Yes Yes Yes (18+)
Sends AVIF in Accept No Yes Yes Yes Yes
Honors Vary: Accept in browser cache Yes Yes Yes Yes Yes
<img srcset> width negotiation Yes Yes Yes Yes Yes
fetchpriority on <img> Yes (15.4+) Yes Yes (102+) Yes (132+) Yes (102+)

Edge capability by platform

Capability Cloudflare Fastly AWS CloudFront
Managed image transformer Yes (Image Resizing) Yes (Image Optimizer) No (build on Lambda@Edge)
Zero-config auto re-encode Yes (Polish) No No
Reads Accept without extra config Yes Needs VCL normalize Needs cache policy
Sub-second global purge Yes Yes No (invalidation lag)
Origin offload mechanism Tiered Cache / Argo Shielding Origin Shield

Where to go next

Start with the platform you run. If you are on Cloudflare, the Image Resizing and Polish guide is the fastest path to a working negotiated pipeline. On AWS, begin with CloudFront cache behaviors for media and get the cache policy right before adding transformation. On Fastly, the VCL negotiation guide covers the normalize-then-transform pattern. Regardless of platform, wire up monitoring and regression detection before you trust the setup in production — an unmonitored edge pipeline is one deploy away from a silent LCP regression.