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.

LCP image response time by cache outcome Bar chart comparing image response times for an edge hit, a shield hit and a full transformation miss. LCP image response time by cache outcome Edge hit 24ms Shield hit 95ms Miss + transform (AVIF) 780ms

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-age of 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

javascript
// 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.

Request path with normalised keys and a shield Flow of an image request through an edge cache, a shield tier and the transformation service. Request path with normalised keys and a shield Request w, q + Accept Normalise key params + fmt bucket Edge cache hit ~95% Shield regional hit Transform once per variant

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.

Cache key components for transformed images Which request components should and should not be part of the cache key for transformed images. Cache key components for transformed images Component In key? Why Path with version hash yes identifies the source image Width, quality, fit yes change the output Format bucket from Accept yes AVIF vs WebP vs JPEG utm_*, fbclid, other params no do not change output Cookies no images are public Raw Accept or client hint values no fragment the cache

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.