DEV Community

Muhaymin Bin Mehmood
Muhaymin Bin Mehmood

Posted on

Your Image CDN Picks the Format. Here's How to See What It Actually Sends

Here is one image URL from a real Shopify store, requested three times. Same URL, same second, three different files:

https://jellinator.com/cdn/shop/files/ShotCups_100Pack.png?width=1200

  Chrome (AVIF+WebP)   image/webp     87 KB   Vary: Accept
  WebP only            image/webp     87 KB   Vary: Accept
  Old browser          image/png    1223 KB   Vary: Accept
Enter fullscreen mode Exit fullscreen mode

The only thing that changed between the three requests is the Accept header. A browser that says it understands WebP gets an 87 KB file. A client that doesn't gets the original 1.2 MB PNG.

That is content negotiation, and most image CDNs do it now. It's great for visitors. It also means that "what format is this image?" has no single answer, and some tools will give you the wrong one.

The header that decides it

Every browser sends an Accept header with image requests. Current Chrome sends:

Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8
Enter fullscreen mode Exit fullscreen mode

A negotiating CDN reads that list and picks the best format it can produce. It then adds Vary: Accept to the response, which tells every cache in between: "this URL has more than one version, keyed by the Accept header".

If you see Vary: Accept on an image response, the server is negotiating. If you don't, you're getting the same file whatever you ask for.

Check it with curl

curl -I sends a HEAD request and prints the headers. Add the header a browser would send:

curl -sI -H "Accept: image/avif,image/webp,image/*,*/*;q=0.8" \
  "https://jellinator.com/cdn/shop/files/ShotCups_100Pack.png?width=1200" \
  | grep -iE "content-type|content-length|vary"
Enter fullscreen mode Exit fullscreen mode

Then run it again without -H. curl's default is Accept: */*, which is exactly the "old browser" case above. If the two answers differ, your CDN negotiates.

A small script for a list of URLs

When you want to check a batch of images, this does the three requests for each URL and prints one line per client:

// check-formats.mjs: what format does each image URL send to different browsers?
// Usage: node check-formats.mjs <image-url> [more urls...]
const ACCEPTS = {
  "Chrome (AVIF+WebP)": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
  "WebP only": "image/webp,image/*,*/*;q=0.8",
  "Old browser": "image/*,*/*;q=0.8",
}

for (const url of process.argv.slice(2)) {
  console.log(`\n${url}`)
  for (const [label, accept] of Object.entries(ACCEPTS)) {
    const res = await fetch(url, { method: "HEAD", headers: { accept } })
    const type = res.headers.get("content-type")
    const size = res.headers.get("content-length")
    const vary = res.headers.get("vary") ?? "-"
    console.log(`  ${label.padEnd(20)} ${type?.padEnd(12)} ${size ? Math.round(size / 1024) + " KB" : "?"}  Vary: ${vary}`)
  }
}
Enter fullscreen mode Exit fullscreen mode

It needs Node 18 or newer (for the built-in fetch). Some servers answer HEAD differently from GET; if a line looks odd, try the same URL with GET.

The size can change the format too

Run the same image at a smaller width and something else happens:

https://jellinator.com/cdn/shop/files/ShotCups_100Pack.png?width=300

  Chrome (AVIF+WebP)   image/avif     14 KB   Vary: Accept
  WebP only            image/webp     16 KB   Vary: Accept
  Old browser          image/png      98 KB   Vary: Accept
Enter fullscreen mode Exit fullscreen mode

At 300px this CDN sent AVIF to Chrome. At 1200px it sent WebP, even though Chrome said it accepts AVIF.

We saw the same pattern across a larger sample. In a study of 199 Shopify product pages, 94% of image files came back as WebP or AVIF, and 97% of those responses carried Vary: Accept. But when we spot-checked the main product photo of 24 stores at 600px, 22 came back as WebP and none as AVIF. AVIF showed up on some small variants, and not consistently.

So "my CDN supports AVIF" and "my product photos are served as AVIF" are two different claims. Check the second one directly.

Why some audits get this wrong

Any tool that fetches your images without a browser-like Accept header sees the "old browser" version. For the image above, that's a 1.2 MB PNG instead of an 87 KB WebP, so the tool reports a problem your visitors never have.

When you read an image audit, check one thing: does it say which Accept header it used? If it doesn't, compare a couple of its numbers with the script above before you start re-exporting files.

The reverse mistake happens too. If you test only with Chrome DevTools, you see the best case. Visitors on old browsers, some email clients and some social link previews may get the original file.

Caching: don't break Vary

Negotiation only works if every cache between the CDN and the visitor respects Vary: Accept. If you put your own proxy or a second CDN in front of your images and it ignores Vary, the first visitor's format gets cached for everyone. An old client's PNG can then be served to every visitor, or an AVIF can reach a client that can't decode it.

If you add a layer in front of an image CDN, either pass Vary: Accept through and key the cache on it, or let the original CDN handle images directly.

A quick checklist

  • Request your main product or hero image with a browser Accept header. Note the Content-Type and size.
  • Request it again without one. If both match, the server doesn't negotiate, and the format you uploaded is the format everyone gets.
  • Look for Vary: Accept on negotiated responses.
  • Try more than one width. The format can change with size.
  • When an audit flags a heavy image, check which Accept header it sent before trusting the number.

If you'd rather scan a whole page than individual URLs, our website image auditor requests the page's images with a current browser's Accept header and shows the type and size it actually got.

This post was drafted with an AI assistant. Every command and output above was run against the live URLs shown, on 2026-10-12.

Top comments (0)