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.

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:
- CloudFront doesn’t consider request headers when caching by defaultOpens in a new window, and it removes the
AcceptheaderOpens in a new window before forwarding a request to your server. - Cloudflare honors
Varyonly when it is configured toOpens in a new window. Its Vary for ImagesOpens in a new window feature, which caches each image format separately, is available on Pro plans and above.
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:
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.
<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:
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.
