CDN Configuration: Automatically Serve WebP Images
Configure Cloudflare, Fastly, AWS CloudFront, and other CDNs to automatically serve WebP images based on the browser Accept header at the edge.
Most major CDNs can automatically serve WebP images based on the browser’s Accept header — either through built-in image optimisation features or edge-side content negotiation rules — eliminating the need to manually maintain two copies of every image. Offloading this to the CDN edge means your origin only ever stores the original JPEG or PNG, while the CDN handles format negotiation, conversion, and caching globally.
Cloudflare Polish #
Cloudflare’s Polish feature automatically converts images to WebP (and strips metadata) for browsers that support it. Enable it directly from the dashboard with no code changes.
- Open the Speed settings — In the Cloudflare dashboard, select your zone and navigate to Speed → Optimization → Content Optimization.
- Enable Polish — Set Polish to Lossy (recommended for photographs) or Lossless (for images that must retain every pixel). Both modes support WebP conversion.
- Enable WebP — Toggle the WebP switch on. Cloudflare will now serve WebP to browsers that include
image/webpin theirAcceptheader.
Polish is available on the Pro plan and above. Converted images are cached at the Cloudflare edge and served with a cf-polished response header confirming the conversion took place.
Check for the cf-polished response header to confirm Polish is active on a given image. A value like cf-polished: webp_failed indicates the source image couldn’t be converted — often because it’s already a WebP or a format Cloudflare doesn’t process (such as SVG).
Cloudflare Image Resizing #
For on-demand resizing and format conversion in a single request, use Cloudflare Image Resizing. Construct the URL by prefixing your image path with /cdn-cgi/image/ and providing transform parameters.
https://example.com/cdn-cgi/image/format=webp,quality=80/images/photo.jpg
You can chain multiple transforms in the parameter list:
https://example.com/cdn-cgi/image/format=webp,quality=80,width=800,fit=cover/images/photo.jpg
Use format=auto to let Cloudflare pick the best format — currently WebP or AVIF — based on the browser’s Accept header:
For one-off conversions outside your CDN pipeline — for example, re-encoding a single WebP asset as AVIF — our free WebP to AVIF converter runs entirely in your browser.
https://example.com/cdn-cgi/image/format=auto,quality=85/images/photo.jpg
Image Resizing is available on the Pro plan and above and requires Workers to be enabled on the zone.
AWS CloudFront #
CloudFront does not include built-in WebP conversion. Implement format negotiation using a Lambda@Edge or CloudFront Function at the viewer-request event.
CloudFront Functions #
CloudFront Functions run at the edge with sub-millisecond latency and are the recommended approach for simple header-based URL rewrites.
// CloudFront Function — viewer-request
function handler(event) {
var request = event.request;
var headers = request.headers;
var accept = headers['accept'] ? headers['accept'].value : '';
if (accept.includes('image/webp')) {
// Rewrite /images/photo.jpg → /images/photo.webp
request.uri = request.uri.replace(/\.(jpe?g|png)$/, '.webp');
}
return request;
}
Associate this function with the Viewer Request event on your distribution’s cache behaviour. Your origin must pre-generate the .webp files at the rewritten paths.
Lambda@Edge #
Use Lambda@Edge when you need more compute — for example, to call an image transformation API or read from a database.
// Lambda@Edge — viewer-request
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const accept = headers['accept']?.[0]?.value || '';
if (accept.includes('image/webp')) {
request.uri = request.uri.replace(/\.(jpe?g|png)$/i, '.webp');
}
return request;
};
Review the AWS documentation on image optimisation with CloudFront for a full reference architecture including S3 origin setup, cache policies, and cache key configuration.
Fastly Edge-Side Content Negotiation #
Use Fastly’s VCL (Varnish Configuration Language) to inspect the Accept header at the edge and route WebP-capable browsers to .webp variants stored on your origin.
# Fastly VCL — serve WebP when browser supports it
sub vcl_recv {
if (req.http.Accept ~ "image/webp") {
set req.http.X-WebP = "1";
}
}
sub vcl_backend_response {
if (req.http.X-WebP == "1") {
set beresp.http.Vary = "Accept";
}
}
Extend vcl_recv to rewrite the URL before the request reaches your origin:
sub vcl_recv {
if (req.http.Accept ~ "image/webp") {
if (req.url ~ "\.(jpe?g|png)(\?.*)?$") {
set req.url = regsub(req.url, "\.(jpe?g|png)", ".webp");
set req.http.X-WebP = "1";
}
}
}
The Vary: Accept Header #
The Vary: Accept response header is the mechanism that tells every cache layer — CDN, reverse proxy, and browser — that the response content varies based on the Accept request header. Without it, a cache stores the first response it receives and serves it to everyone, regardless of what format they actually requested.
Missing
Vary: Acceptcauses cache poisoning. If a WebP-capable browser is the first to request an image, the CDN caches the WebP response and then serves it to Safari 13, IE11, and any other browser that requested the same URL — even though those browsers can’t render WebP. Always confirmVary: Acceptis present in your image responses.
Set the header at your origin server so it propagates through every cache layer automatically:
# Nginx — add to image location block
add_header Vary Accept;
# Apache — add to .htaccess or virtual host
Header append Vary Accept
Verifying CDN WebP Delivery #
Use curl to confirm your CDN is serving WebP to supporting browsers and the correct fallback to others.
# Check what format the CDN returns for a WebP-supporting client
curl -H 'Accept: image/webp,image/*,*/*' -I https://example.com/photo.jpg
# Look for: content-type: image/webp
# Confirm a non-WebP client still receives JPEG
curl -H 'Accept: image/jpeg,image/*,*/*' -I https://example.com/photo.jpg
# Look for: content-type: image/jpeg
# Verify the Vary header is present in both responses
curl -H 'Accept: image/webp,image/*,*/*' -I https://example.com/photo.jpg | grep -i 'vary\|content-type'
Expected WebP Response #
content-type: image/webp
vary: Accept
cache-control: public, max-age=31536000
Expected Fallback Response #
content-type: image/jpeg
vary: Accept
cache-control: public, max-age=31536000
If content-type returns image/jpeg even when you send Accept: image/webp, verify that your CDN feature is enabled, your origin is generating WebP files, and the Accept header is included in your cache key (Vary configuration).
