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.
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: Accepttells every downstream RFC-9111 cache (the CDN, corporate proxies, the browser cache) to store a separate object per distinctAcceptvalue. It is honest and standards-compliant, but becauseAcceptvalues vary wildly between clients, a literalVary: Acceptcan itself shatter the cache unless the CDN normalizesAcceptbefore applying it.- A normalized cache key keeps the key internal to the CDN: the edge computes
avif|webp|jpegfromAccept, 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.
Related
- Cloudflare Image Resizing and Polish —
/cdn-cgi/image/transforms, Workerscf.image, and zero-config Polish - AWS CloudFront Cache Behaviors for Media — cache policies, Lambda@Edge routing, and cache-miss diagnosis
- Fastly VCL for Image Format Negotiation — normalizing Accept and driving the Image Optimizer from VCL
- Monitoring & Regression for Media Delivery — Lighthouse CI budgets, WebPageTest diffs, and CrUX field tracking
- Cache-Control Headers for Image and Video Assets — the origin-side max-age, immutable, and Vary contract the edge builds on
- MIME Type Configuration for Modern Media Servers — correct Content-Type registration for AVIF and WebP masters