| |

Why WebP Delivery Can Break Behind a CDN (and How the picture Element Fixes It)

There are two common ways to serve WebP to browsers that support it. One decides on the server, by reading the request’s Accept header and swapping the file. The other lets the browser decide, using a <picture> element.

Both work as long as every request reaches your server. Put a cache or CDN in between, and the server-side approach depends on whether that cache honors Vary: Accept.

WebberZone featured image: Why WebP Delivery Can Break Behind a CDN

How Accept-header rewriting works

A browser’s Accept header lists the formats it understands. If it includes image/webp and a WebP copy exists, the server returns WebP. Otherwise, it returns the original.

So photo.jpg returns WebP to one visitor and JPEG to the next.

Why the cache needs Vary: Accept

By default, a cache stores responses by URL. Vary: Accept tells it to keep a separate copy for each Accept value.

Fastly follows standard Vary handling, though its documentation warnsOpens in a new window that Accept has so many distinct values that varying on it can reduce cache performance. Other CDNs need configuring:

So whether Accept-header rewriting works behind a CDN depends on your CDN setup.

What happens when a cache ignores Vary

Take a cache that passes Accept through to your server but stores responses by URL alone. The first response wins. If the first visitor’s browser accepted WebP, every later visitor gets WebP, including clients that can’t display it.

If the first visitor got the JPEG, everyone gets the JPEG. Nothing looks broken, but you lose the savings. Rewrite rules behind a default CloudFront setup always end up here, because your server never sees image/webp.

How the picture element avoids it

<picture> moves the decision into the browser. Every visitor gets the same HTML, with a <source> for each format and the original <img> as the fallback. Here is real WebberZone Image Optimizer output, with line breaks added:

<picture>
	<source type="image/webp" srcset="https://example.com/wp-content/uploads/2026/10/photo-300x200.jpg.webp" />
	<img data-wzio-processed="1" src="https://example.com/wp-content/uploads/2026/10/photo-300x200.jpg" alt="">
</picture>

The browser uses the first format it supports, or falls back to the <img>.

Without server rules, each format has its own URL that always returns the same file. The page cache stores one version of the page, the CDN stores one copy of each file, and nothing depends on Vary. That’s why I built Image Optimizer around <picture> from the start.

How Image Optimizer applies it

Delivery settings are under Media → Image Optimizer → Delivery. By default, the plugin wraps images in post content and images output through WordPress functions, such as featured images and galleries. The optional Whole page (buffered) setting, on WordPress 6.9 or later, also catches images printed by page builders or templates.

Only images from your uploads folder are wrapped, and only when an optimized copy exists. Otherwise, the original <img> is left alone and can be queued for conversion. Nothing is converted while the page is being built.

The exception: CSS background images

<picture> needs an <img> to wrap. Background images set in a stylesheet never reach WordPress’s image filters, and the plugin doesn’t rewrite stylesheets.

For those, the CSS background images section of the Delivery tab generates optional Apache and nginx rules. They’re the only part of the plugin that uses Accept rewriting, and both send Vary: Accept. On Apache, you can add them to .htaccess; nginx rules are added by hand. Copy them from the Delivery tab.

The rules apply to every JPEG, PNG, or GIF with an optimized copy, including the <img> fallbacks inside <picture>. If your CDN doesn’t respect Vary: Accept, leave them out. Everything served through <picture> keeps working.

If you added nginx rules from an earlier version, regenerate them. Older rules served the original instead of WebP when the browser accepted AVIF, but no AVIF copy existed.

To check, request an image with and without WebP accepted:

curl -sI -H "Accept: image/webp" https://example.com/wp-content/uploads/2026/10/photo.jpg | grep -iE "content-type|vary"
curl -sI -H "Accept: image/jpeg" https://example.com/wp-content/uploads/2026/10/photo.jpg | grep -iE "content-type|vary"

You should see image/webp and then image/jpeg, both with Vary: Accept. Repeat against your CDN after purging that image. If your server answered correctly but the CDN returns the same type both times, the CDN isn’t keeping the formats apart.

Get started

WebberZone Image Optimizer is free. Install it from WordPress.orgOpens in a new window. Delivery through <picture> starts as soon as you activate it. New uploads are converted as they arrive, and you can convert existing images from Media → Bulk Optimize.

Leave a Reply

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