Rolling out AVIF without breaking your CDN

Lead

Converting your catalog to AVIF is the easy part. The part that gets AVIF CDN rollouts reverted is caching: behind a CDN or Varnish, a single misconfigured header can cache an AVIF file against a URL and then serve it to a browser that can’t decode it — and once that bad entry is cached, it’s wrong for everyone who hits it until it expires.

The mechanism that prevents this is one HTTP header, Vary: Accept. This article is about getting it right on the way to a live store, and rolling out your AVIF CDN config in an order that fails safe.

Why now

Most Magento stores sit behind a CDN — Cloudflare being the most common — and the browser’s own cache below that. Content negotiation (serving the best image format per the browser’s Accept header) is exactly the kind of feature that interacts badly with shared caching when the cache key is wrong. As AVIF CDN adoption becomes routine, the Vary footgun is the single most common reason a format rollout goes sideways. Worth understanding before, not after.

Baseline: what goes wrong without Vary

A shared cache stores responses keyed on the URL. With content negotiation, the same URL — photo.jpg — can return three different things depending on the request’s Accept header: the AVIF sidecar, the WebP sidecar, or the original JPEG.

If the cache doesn’t know that, it stores whichever variant it saw first and serves it to everyone. The first visitor is on a modern browser, the cache stores AVIF, and the next visitor on an older client gets handed an AVIF it can’t render — a broken image, served from cache, for hours.

Vary: Accept tells every cache in the chain: "this URL has multiple representations; key them by the Accept header." That single header is the difference between a working AVIF CDN setup and a cached mess.

Tradeoffs

With Vary: Accept set correctly

  • Strength: fully transparent. Every client gets a format it can read, cached and fast, with no markup changes.

The costs you take on

  • Cache variant multiplication. Vary: Accept keys entries on a header that browsers send in many slightly different forms, which can fragment your cache into more variants than the three formats imply. CDNs handle this differently — some normalize Accept for you, some don’t.
  • Per-provider configuration. "Set Vary: Accept" means different things on Cloudflare vs Fastly vs Varnish. You have to verify each layer you run, not assume.
  • A real rollback path. Because the failure mode is cached, you want to be able to convert a subset first and watch, not flip the whole catalog at once.

Working example: the safe rollout

Don’t big-bang it. Roll out in stages so any problem shows up on a slice, not the whole store.

1. Set the header first, verify it in isolation. Before converting anything broadly, confirm Vary: Accept is present on image responses at the origin (the nginx config emits it) and preserved through every cache layer. A CDN that strips or ignores Vary is the thing you most need to catch here.

Both requests should return Vary: Accept. If the second one doesn’t, Cloudflare is stripping the header — stop and fix that before converting anything.

2. Convert a subset. Run the converter on one category or a few thousand images, not the catalog. Leave the rest as originals.

3. Watch the right signals. Pull image responses with different Accept headers and confirm each cache returns the correct variant and reports a sensible cache status.

Run each twice — the second request should show cf-cache-status: HIT for its own variant. If both return the same Content-Type, your AVIF CDN cache key is wrong; stop widening the rollout until Vary is confirmed end-to-end.

4. Scale up. Once the subset is clean across your cache layers, widen the conversion. At catalog scale this is where async conversion via the queue earns its keep — see Related reading.

Per-layer notes:

  • Cloudflare. Cloudflare respects Vary: Accept and normalizes the Accept header into image-format buckets, which keeps cache fragmentation in check. Behaviour differs across plans — confirm on yours before assuming it’s handled automatically.
  • No CDN (browser cache only). Vary: Accept still belongs on the response. Without it, a browser that cached an AVIF will serve it from its own cache in contexts that shouldn’t get it. The failure is per-client rather than shared-cache-wide, but the header is still required.
  • A note on Varnish. In Magento, Varnish is the Full Page Cache — it stores rendered HTML pages, not static assets. Images bypass Varnish entirely and are served from the origin through the CDN to the browser. No Varnish VCL changes are needed for image content negotiation.

What to skip

  • Don’t convert the whole catalog on day one. A staged rollout costs you a day and saves you a cached-everywhere incident.
  • Don’t assume the CDN honours Vary. Test it. "It works on the origin" is not the same as "it works through the edge."
  • Don’t set Vary: * or vary on headers you don’t need — you’ll shred your cache hit rate for no benefit. Vary: Accept, scoped to image paths, is the whole job.

Verification

Check that each cache layer stores and serves variants correctly, not just the origin.

curl -sI -H 'Accept: image/avif,image/webp,*/*' \
  https://your-store/media/catalog/product/x/y/photo.jpg \
  | grep -iE 'content-type|vary|cf-cache-status|x-cache|age'

curl -sI -H 'Accept: text/html' \
  https://your-store/media/catalog/product/x/y/photo.jpg \
  | grep -iE 'content-type|vary|cf-cache-status|x-cache|age'

You’re looking for: Vary: Accept on both, Content-Type: image/avif on the first and image/jpeg on the second, and a cache-status header that shows each variant cached independently. If both requests return the same Content-Type, your AVIF CDN cache key is wrong — stop and fix Vary before going wider.

Related reading

Related Topics

← Previous
Next →

Written in collaboration with AI (Claude, by Anthropic). Ideas, verification, and accountability are mine; research and drafting are AI-assisted. Full disclosure → · Found an error? Tell me.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *