?format=match is Accept-negotiated at the origin but the response is cached under the URL alone, so the first client to warm a key pins the output format for every later client, for the lifetime of Cache-Control: public,max-age=31536000,immutable.
The origin is correct
Same URL, two Accept headers, straight to the service:
Accept: image/avif,image/webp,image/*,*/*;q=0.8 -> image/avif 67,770 bytes
Accept: image/jpeg,image/png,image/*;q=0.8 -> image/png 757,836 bytes
Proper negotiation, and Vary: Accept is set on the response. parseOptions folds Accept into options.format before getImageKey runs, so an AVIF/WebP client resolves to an explicit format and its store key carries it, while a client that negotiated neither stays Match and every such client wants the same bytes. Nothing to fix here.
The CDN does not honour Vary: Accept
Served through the CDN, on a key nobody had requested before, warmed by a modern client first:
1 Accept: avif,webp,... -> image/avif 67,770 B cf=MISS Vary=Accept
2 Accept: jpeg,png only -> image/avif 67,770 B cf=HIT Vary=Accept
3 Accept: jpeg,png only (again) -> image/avif 67,770 B cf=HIT Vary=Accept
Step 2 is a client that explicitly did not offer avif or webp being handed AVIF.
It runs the other way too, and that direction is the expensive one. On an aged key warmed by a non-AVIF client, three different Accept headers all received the same image/png 688,435 bytes where a fresh key renders image/avif 29,010 bytes. That is a 23x bandwidth penalty pinned for a year.
Reproduce with any ?format=match URL at a width that has never been requested, so the key starts cold.
Scale
Sampling distinct sized keys from a single day of origin access logs: 86% wrap .png sources, and 24 of 30 sampled keys were serving a large image/png where a fresh key rendered a small image/avif. So this is the common case for PNG-sourced images rather than an edge case.
(The /p/ token is plain base58 of the source URL, so a key's source extension can be classified offline without fetching anything.)
Why cache invalidation is not the answer
Purging just re-opens the race: whichever client warms the key next re-pins the format for a year. Any fix has to make the cached identity carry the format.
Options
- Edge: bucket
Accept into the cache key (a small normalisation to something like avif / webp / legacy rather than the raw header, which is too high-cardinality to cache well).
- Code: stop depending on
Accept at a cacheable URL. Resolve format=match to an explicit format=avif|webp|... URL, so the URL itself is the format identity and the CDN cannot conflate two clients. This is the durable one and it is why I am filing here rather than treating it purely as CDN config.
- Do nothing for the correctness direction specifically. A client sending
image/* is treated as having told us nothing and keeps the modern format, so the population actually harmed by receiving AVIF is small. The bandwidth direction is the one worth money.
Option 2 interacts with getImageKey's Fit+Match branch and with every caller that builds format=match URLs (@ecency/render-helper's proxifyImageSrc and buildSrcSet), so it is not a small change and is worth designing before starting.
Found while sweeping for stale animated-GIF keys after #58; unrelated to that work and predates it.
?format=matchis Accept-negotiated at the origin but the response is cached under the URL alone, so the first client to warm a key pins the output format for every later client, for the lifetime ofCache-Control: public,max-age=31536000,immutable.The origin is correct
Same URL, two Accept headers, straight to the service:
Proper negotiation, and
Vary: Acceptis set on the response.parseOptionsfolds Accept intooptions.formatbeforegetImageKeyruns, so an AVIF/WebP client resolves to an explicit format and its store key carries it, while a client that negotiated neither staysMatchand every such client wants the same bytes. Nothing to fix here.The CDN does not honour
Vary: AcceptServed through the CDN, on a key nobody had requested before, warmed by a modern client first:
Step 2 is a client that explicitly did not offer avif or webp being handed AVIF.
It runs the other way too, and that direction is the expensive one. On an aged key warmed by a non-AVIF client, three different Accept headers all received the same
image/png688,435 bytes where a fresh key rendersimage/avif29,010 bytes. That is a 23x bandwidth penalty pinned for a year.Reproduce with any
?format=matchURL at a width that has never been requested, so the key starts cold.Scale
Sampling distinct sized keys from a single day of origin access logs: 86% wrap
.pngsources, and 24 of 30 sampled keys were serving a largeimage/pngwhere a fresh key rendered a smallimage/avif. So this is the common case for PNG-sourced images rather than an edge case.(The
/p/token is plain base58 of the source URL, so a key's source extension can be classified offline without fetching anything.)Why cache invalidation is not the answer
Purging just re-opens the race: whichever client warms the key next re-pins the format for a year. Any fix has to make the cached identity carry the format.
Options
Acceptinto the cache key (a small normalisation to something likeavif/webp/legacyrather than the raw header, which is too high-cardinality to cache well).Acceptat a cacheable URL. Resolveformat=matchto an explicitformat=avif|webp|...URL, so the URL itself is the format identity and the CDN cannot conflate two clients. This is the durable one and it is why I am filing here rather than treating it purely as CDN config.image/*is treated as having told us nothing and keeps the modern format, so the population actually harmed by receiving AVIF is small. The bandwidth direction is the one worth money.Option 2 interacts with
getImageKey'sFit+Matchbranch and with every caller that buildsformat=matchURLs (@ecency/render-helper'sproxifyImageSrcandbuildSrcSet), so it is not a small change and is worth designing before starting.Found while sweeping for stale animated-GIF keys after #58; unrelated to that work and predates it.