How to Cache Transformed Images at the Edge
This guide extends Image CDNs and fetchpriority, part of Image & Media Optimization. An image transformation — fetch the original, decode, resize, encode as AVIF — can take hundreds of milliseconds to over a second. That is fine once per variant, and terrible when it happens on the critical path for the LCP image of a real user. Whether a transformed image comes from the edge cache in 20ms or from a transformation in 800ms depends almost entirely on cache keys, URL design, and how the cache is layered.
The common problems are cache fragmentation (keys that include irrelevant query parameters or raw headers), short TTLs on images that never change, Vary headers that split caches, and purges that wipe out every variant at once. Fix those and hit rates above 95% are typical, with LCP images rarely touching the transformation service.
Rapid Diagnosis
- Read cache status headers (
CF-Cache-Status,X-Cache,Age) on image responses across several page loads and locations. - Compute hit ratio for image paths in CDN analytics, separately from HTML and API.
- List cache key components for image routes: query parameters, headers, cookies.
- Check TTLs. Images with
max-ageof minutes or hours are re-validated or re-transformed unnecessarily.
Root Cause Analysis
1. Fragmented keys. Tracking parameters, parameter order, or unneeded headers create many keys for one image.
2. Raw Vary. Vary: Accept with full Accept strings, or Vary on client hints, multiplies entries.
3. Short TTLs. Unversioned image URLs force short lifetimes, so caches expire constantly.
4. Flat cache topology. Each edge location misses independently, multiplying transformations by the number of locations.
Step-by-Step Resolution
1. Version URLs and cache forever
Include a content hash or version in the image URL and serve Cache-Control: public, max-age=31536000, immutable. Changing the image changes the URL; nothing needs purging.
2. Normalise the cache key
// Edge worker: build a minimal cache key for transformed images.
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const keep = ['w', 'q', 'fit']; // only parameters that change output
const params = new URLSearchParams();
for (const k of keep) if (url.searchParams.has(k)) params.set(k, url.searchParams.get(k));
params.sort();
const accept = request.headers.get('accept') || '';
const fmt = accept.includes('image/avif') ? 'avif' : accept.includes('image/webp') ? 'webp' : 'jpeg';
const key = new Request(`${url.origin}${url.pathname}?${params}&fmt=${fmt}`);
const cache = caches.default;
let res = await cache.match(key);
if (!res) {
res = await fetch(`${env.TRANSFORM}${url.pathname}?${params}&f=${fmt}`);
res = new Response(res.body, res);
res.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
ctx.waitUntil(cache.put(key, res.clone()));
}
return res;
},
};
// trade-off: normalising Accept into three buckets means browsers that support a
// format only partially get the bucket's choice; this is rarely a real problem.
3. Add a shield tier
Enable tiered caching or an origin shield so edge misses go to a regional cache before the transformation service. One transformation then serves the whole network.
4. Purge narrowly, if at all
Purge by URL or surrogate key for a specific image, never by wildcard for all images. With versioned URLs, purges should be rare.
Verification
Request the same image with extra tracking parameters and different Accept strings that map to the same format; each should hit the same cache entry (HIT and increasing Age). Hit ratio for image paths should exceed 95% after warm-up. The transformation service's request rate should approximate the rate of genuinely new variants, not total traffic. In RUM, LCP resourceLoadDuration should be low and stable across regions.
Worked Example: A News Publisher
A news publisher's image CDN showed a 61% hit ratio on image paths. Investigation found three causes: social sharing added utm_* and fbclid parameters to image URLs embedded in shared HTML; Vary: Accept used raw Accept strings, producing dozens of variants per image; and image URLs were unversioned with a one-hour TTL. The team stripped unknown parameters from the key, normalised Accept into three format buckets, moved to hashed URLs with one-year immutable caching, and enabled origin shield. Hit ratio rose to 97%, transformation requests fell by 92%, and LCP p75 on article pages improved by 340ms, mostly in regions far from the transformation service.
Warming Caches for Predictable Traffic
Some image traffic is predictable: a homepage hero swapped at 9am, a product drop at noon, a newsletter that sends a million readers to the same article. For these, warm caches before the traffic arrives. After publishing, request the key variants (common widths × formats) through each major region, or at least through the shield tier, with a small script. With a shield, warming one location per region is often enough. This turns the first thousand users' LCP from a transformation into a cache hit, and protects the transformation service from a sudden burst of identical misses — a thundering herd that request coalescing at the CDN may only partly absorb.
Monitoring Cache Health Over Time
Hit rates degrade quietly. A new marketing tool appends a tracking parameter, a template change introduces a new width, or a CDN configuration update drops a key normalisation rule. Add three ongoing checks: a dashboard of edge and shield hit ratio for image paths with an alert below your baseline, a daily report of the top cache keys by miss count (which usually reveals the new parameter or width immediately), and the transformation service's request rate compared with new uploads. If transformations grow faster than uploads, something is fragmenting the cache. Catching that in days rather than months keeps LCP images on the fast path.
Common Mistakes
- Purging all images on deploy. Triggers a transformation storm.
- Including cookies in image cache keys. Every user gets a private cache entry.
- Short TTLs on unversioned URLs. Constant re-transformation.
- Ignoring shield. Each of hundreds of edge locations transforms independently.
Edge Cases
Signed URLs. Signatures in the path are part of the key; make sure signatures are deterministic, or every page render creates new keys.
Private images. Use token authentication at the edge with a key that excludes the token, so authorised users share the cached object.
Very large catalogues of rarely viewed items. Rarely viewed images will always miss at the edge; a shield and storage-backed cache tier help most here.
Format changes. When enabling a new format, the cache fills gradually; expect a temporary rise in transformations.
FAQ
What hit ratio should transformed images reach?
Above 95% is typical for sites with versioned URLs and normalised keys. Sites with huge, rarely viewed catalogues may sit lower at the edge but high at the shield.
Should I use Vary: Accept?
Only if the CDN normalises it. Otherwise, normalise Accept into a format bucket in the cache key yourself, or put the format in the URL.
How long should transformed images be cached?
With versioned URLs, a year with immutable. Without versioning, TTLs must be short, which is the main reason to add versioning.
Does origin shield add latency?
Slightly on misses at the edge, but it greatly reduces transformations and origin load. Overall image latency usually improves.
Can I cache transformed images in the browser too?
Yes — the same immutable Cache-Control applies to browsers, so repeat visits avoid the network entirely.
What about request coalescing?
Many CDNs collapse simultaneous misses for the same key into one origin request. It helps with bursts but is not a substitute for warming or shielding.
Related
- Normalizing cache keys to raise hit rate — the general technique.
- Tiered caching and origin shield — reducing duplicate misses.
- Fixing a slow LCP image behind an image CDN — diagnosing the miss path.