Cloudflare Image Resizing and Polish
Cloudflare gives you two distinct edge image tools that people constantly confuse, and choosing wrong costs either money or missed compression. This guide is part of CDN & Edge Media Delivery and pins down exactly what each does: Polish re-encodes the image you already serve into WebP or AVIF with zero URL changes and no resizing, while Image Resizing derives arbitrary widths, crops, and formats from a master through a /cdn-cgi/image/ URL or a Worker’s cf.image options. Polish is a checkbox; Image Resizing is a transformation engine you invoke per request. They can run together, and knowing where one ends and the other begins is the whole game.
Concept & architecture
Polish: automatic re-encoding in place
Polish operates on the images your origin already serves at their existing URLs. When enabled in the dashboard, Cloudflare intercepts image responses, re-compresses them, and — in Lossy or WebP mode — can hand back a smaller WebP or AVIF to clients whose Accept header advertises support. There is no width change, no crop, no new URL: https://site.com/img/hero.jpg stays exactly that, but the bytes on the wire become image/webp for a Chrome client and stay image/jpeg for a client that cannot decode WebP. Polish reads Accept and negotiates format for you, and it sets a cf-polished response header describing what it did.
Polish has three settings. Off does nothing. Lossless re-compresses without discarding data — smaller PNGs, no quality change. Lossy applies quality reduction and is the mode most sites want for photographs. Independently, the WebP toggle (and AVIF, where available) lets Polish switch the delivered format based on Accept. Because Polish never resizes, it cannot fix an oversized image: a 4000-pixel master delivered into a 400-pixel slot is still 4000 pixels after Polish, just in a more efficient codec.
Image Resizing: derivations from a master
Image Resizing is the transformation engine. You express a derivation — width, height, fit mode, quality, format — and Cloudflare fetches the master, applies it at the edge, caches the result, and serves it. There are two ways to invoke it:
- The URL form:
/cdn-cgi/image/<options>/<source>, where<options>is a comma-separated list likewidth=640,quality=75,format=autoand<source>is the path or absolute URL of the master. - The Workers form:
fetch(url, { cf: { image: { … } } }), which is the same engine driven programmatically, so you can compute options per request, add signed-URL checks, or gate formats behind flags.
Unlike Polish, Image Resizing requires either a paid plan tier for the URL form or a Worker, and it bills per unique transformation (not per delivery from cache). The distinction and the cost model are covered in depth in Cloudflare Polish vs Image Resizing: tradeoffs.
The diagram shows both paths and where format=auto reads the Accept header.
Parameter and benchmark reference
The numbers below come from a 3000×2000 photographic JPEG master (1.9 MB) delivered into a 640-pixel-wide slot, measured against a warm edge cache. “Polish only” leaves the image at native resolution; “Resizing” derives the 640-pixel variant. Byte figures are the delivered payload seen by the browser.
| Path | Delivered width | Format (Chrome) | Payload | vs raw master | Notes |
|---|---|---|---|---|---|
| No optimization | 3000 px | JPEG | 1.9 MB | — | oversized for a 640 px slot |
| Polish Lossy + WebP | 3000 px | WebP | ~1.2 MB | −37% | smaller codec, still oversized dimensions |
| Polish Lossy + AVIF | 3000 px | AVIF | ~0.9 MB | −53% | best codec, dimensions unchanged |
Resizing width=640,format=auto |
640 px | AVIF | ~46 KB | −97% | correct pixels + best codec |
Resizing width=640,quality=60 |
640 px | AVIF | ~31 KB | −98% | aggressive quality for thumbnails |
Tradeoff: Polish alone is a large win for free but leaves the biggest lever — pixel dimensions — untouched. The 3000-pixel image is still decoded at 3000 pixels on the client even though it paints into 640, wasting decode time and memory. Resizing addresses dimensions; that is why the payload drops two orders of magnitude only in the Resizing rows.
Core option reference
| Option | Applies to | Meaning |
|---|---|---|
width / height |
Resizing | Target dimensions in device pixels. Bound these to real breakpoints. |
format=auto |
Resizing | Negotiate AVIF/WebP/JPEG from the request Accept header. |
quality |
Resizing | Perceptual quality 1–100. ~75 photographic default; lower for thumbnails. |
fit |
Resizing | How the image fits the box: scale-down, contain, cover, crop, pad. |
gravity |
Resizing | Focal point for cover/crop — auto, left, top, or 0.5x0.3 coords. |
dpr |
Resizing | Device-pixel-ratio multiplier applied on top of width. |
sharpen |
Resizing | 0–10 unsharp-mask strength to recover crispness lost in downscaling. |
metadata |
Resizing | none (default), copyright, or keep for EXIF handling. |
| Polish: Lossy/Lossless | Polish | Whole-zone re-compression mode; no per-request control. |
| Polish: WebP | Polish | Enables Accept-driven WebP/AVIF swap on in-place images. |
Step-by-step implementation
Step 1 — Enable Polish (the zero-config win)
Polish needs no code. In the Cloudflare dashboard, under Speed → Optimization → Image Optimization, set Polish to Lossy and enable WebP. From that moment every image response your origin emits is re-encoded per Accept. Verify with a request that advertises WebP:
# -H forces an Accept header that advertises AVIF and WebP, like Chrome 121.
# A working Polish returns content-type: image/webp (or image/avif) plus a
# cf-polished header describing the original size and the saving.
curl -sI -H 'Accept: image/avif,image/webp,*/*;q=0.8' \
https://yoursite.com/img/hero.jpg \
| grep -iE 'content-type|cf-polished'
# Expected:
# content-type: image/webp
# cf-polished: origFmt=jpeg, origSize=1900000, status=webp_bigger? no ...
Warning: Polish only acts on images served from your origin with a cacheable response. Images already behind /cdn-cgi/image/ are handled by Image Resizing instead, and Polish will not double-process them. If cf-polished is absent, the response was uncacheable (check Cache-Control: no-store) or the file was already smaller in its original format.
Step 2 — Serve a resized, negotiated image via the URL form
For correct dimensions, switch to Image Resizing. The URL form works on any zone with the feature enabled:
<!--
/cdn-cgi/image/ is intercepted at Cloudflare's edge. The options segment:
width=640 derive a 640-device-pixel variant from the master
quality=75 perceptual quality target
format=auto negotiate AVIF/WebP/JPEG from the Accept header
fit=scale-down never enlarge beyond the master's intrinsic width
The trailing /img/hero-master.jpg is the ORIGIN path of the master image.
-->
<img
src="/cdn-cgi/image/width=640,quality=75,format=auto,fit=scale-down/img/hero-master.jpg"
srcset="/cdn-cgi/image/width=640,quality=75,format=auto/img/hero-master.jpg 640w,
/cdn-cgi/image/width=1280,quality=75,format=auto/img/hero-master.jpg 1280w,
/cdn-cgi/image/width=1920,quality=75,format=auto/img/hero-master.jpg 1920w"
sizes="(max-width: 700px) 100vw, 640px"
width="640" height="400"
alt="Hero product photograph"
fetchpriority="high">
Step 3 — Drive resizing from a Worker for conditional logic
When you need per-request decisions — signed URLs, an AVIF kill-switch, path-based quality — a Worker calls the same engine through cf.image:
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Only transform image routes; pass everything else straight through.
if (!url.pathname.startsWith('/img/')) return fetch(request);
// Clamp width to an allowlist. Passing arbitrary client-supplied widths
// straight through would mint a new cached variant (and a new billed
// transform) for every pixel value an attacker or a buggy client sends.
const requested = Number(url.searchParams.get('w')) || 640;
const allowed = [320, 640, 960, 1280, 1920];
const width = allowed.reduce((a, b) =>
Math.abs(b - requested) < Math.abs(a - requested) ? b : a);
const accept = request.headers.get('Accept') || '';
// An explicit format choice lets us disable AVIF via env flag without a
// redeploy of markup. format:'auto' would also negotiate for us.
const format = (env.AVIF_ENABLED === 'true' && accept.includes('image/avif'))
? 'avif'
: accept.includes('image/webp') ? 'webp' : 'baseline-jpeg';
return fetch('https://origin.example.com' + url.pathname, {
cf: {
image: {
width,
quality: 75,
format,
fit: 'scale-down', // never upscale
sharpen: 1, // recover a little crispness after downscale
metadata: 'none', // strip EXIF to shave bytes
},
cacheEverything: true,
cacheTtl: 31536000, // cache the derived variant for a year
},
});
},
};
Tradeoff: the Worker form costs a Worker invocation on top of the transform, but it is the only way to clamp untrusted width input, sign URLs, or roll AVIF out gradually. For static markup with fixed breakpoints, the URL form in Step 2 is cheaper and simpler.
Step 4 — Combine Polish and Resizing deliberately
Polish and Resizing coexist, but understand the boundary: once a URL is a /cdn-cgi/image/ transformation, Resizing owns the format via format=auto, and Polish does not re-touch it. Leave Polish on for the broad set of images that are not wrapped in a resizing URL (user-uploaded avatars served directly, legacy <img src> tags you have not migrated), and use Resizing for the hero and gallery images where dimensions matter. The tradeoffs guide walks the decision in detail.
Parameter reference: fit and gravity
fit is the most misunderstood option because it changes both dimensions and cropping:
scale-down— resize down to fit withinwidth×height, never up. Preserves aspect ratio. The safe default.contain— resize to fit within the box, may upscale. Preserves aspect ratio; can produce a smaller-than-box result on one axis.cover— fill the entire box, cropping overflow. Preserves aspect ratio; combine withgravityto choose what survives the crop.crop— likecoverbut also shrinks the box to the exactwidth×height, hard-cropping.pad— resize to fit and pad the remainder to the exact box withbackgroundcolour.
The full option string, including dpr, onerror, anim, and background, is documented parameter-by-parameter in Configuring Cloudflare Image Resizing URL parameters.
Tradeoffs & edge cases
Tradeoff: Polish saves bytes but not decode work. Because it never resizes, a Polish-only pipeline still ships full-resolution pixels the device must decode and hold in memory. On image-dense pages this inflates INP even as the payload shrinks. Reserve Polish-only for images already served near their display size.
Warning: format=auto needs the Accept header to survive. If a Worker or an upstream proxy strips or overwrites Accept before the transform runs, format=auto falls back to serving the source format to everyone. Log request.headers.get('Accept') in the Worker when negotiation misbehaves.
Tradeoff: every unique option string is a separate billed transform and cache entry. width=640 and width=641 are two transforms. Standardize on a fixed breakpoint set and never interpolate widths from continuous client input — clamp to the allowlist as in Step 3.
Warning: Resizing does not read your origin’s Cache-Control for the derived variant. The derived object’s TTL comes from cacheTtl/cacheEverything (Worker) or the zone’s Browser Cache TTL, not from the master’s headers. Set the master path to be content-hashed so a new asset is a new URL rather than relying on invalidation. This is the same immutable-URL discipline described in Cache-Control headers for image and video assets.
Tradeoff: fit=cover with gravity=auto uses saliency detection, which is not free and not perfect. For product images where the subject must never be cropped out, prefer explicit gravity coordinates over auto.
Browser & CDN compatibility
Format decode is a browser property; Cloudflare only decides which format to hand over based on Accept.
| Feature | Safari 14 | Safari 16 | Chrome 85+ | Firefox 93+ | Edge 18+ |
|---|---|---|---|---|---|
| Receives WebP from Polish/Resizing | Yes | Yes | Yes | Yes | Yes |
| Receives AVIF from Polish/Resizing | No | Yes | Yes | Yes | Yes (18+) |
Sends image/avif in Accept |
No | Yes | Yes | Yes | Yes |
srcset width selection on /cdn-cgi/image/ URLs |
Yes | Yes | Yes | Yes | Yes |
fetchpriority honored on resized <img> |
Yes (15.4+) | Yes | Yes (102+) | Yes (132+) | Yes (102+) |
Debugging & validation
Confirm what actually reached the browser
The two diagnostic headers are cf-polished (Polish acted) and cf-resized (Image Resizing acted). They are mutually exclusive for a given response.
# Ask for AVIF explicitly and inspect which engine handled the request and
# what content-type came back. cf-resized appears only on /cdn-cgi/image/ URLs.
curl -sI -H 'Accept: image/avif,image/webp,*/*;q=0.8' \
'https://yoursite.com/cdn-cgi/image/width=640,format=auto/img/hero-master.jpg' \
| grep -iE 'content-type|cf-resized|cf-cache-status'
# Expected:
# content-type: image/avif
# cf-resized: internal stats (full=…, n=…, q=…)
# cf-cache-status: HIT
If content-type comes back as image/jpeg when you asked for AVIF, either format=auto was omitted from the option string or the Accept header did not reach the transform. If cf-cache-status: MISS persists across identical requests, your option string is varying (interpolated width) or the cache is being bypassed.
Verify Polish did not make things worse
# Polish occasionally finds the re-encoded output LARGER than the source
# (already-optimized images). cf-polished reports this; when it does,
# Cloudflare serves the original. Grep for the status token to confirm.
curl -sI -H 'Accept: image/webp,*/*' https://yoursite.com/img/logo.png \
| grep -i cf-polished
# A "webp_bigger" or similar note means the original was kept — expected for
# small flat-colour PNGs where WebP has no advantage.
Compare payloads across formats
# Download the AVIF and WebP variants and compare bytes. -o writes the body;
# -w prints the transferred size so you can diff without opening the files.
for fmt in "image/avif" "image/webp" "image/jpeg"; do
printf '%s: ' "$fmt"
curl -s -H "Accept: $fmt" \
'https://yoursite.com/cdn-cgi/image/width=640,format=auto/img/hero-master.jpg' \
-o /dev/null -w '%{size_download} bytes\n'
done
Related
- Configuring Cloudflare Image Resizing URL parameters — the full
/cdn-cgi/image/option string, one worked example annotated end to end - Cloudflare Polish vs Image Resizing: tradeoffs — decision matrix, plan and cost considerations, when to combine the two
- CDN & Edge Media Delivery — the negotiation, cache-key, and shielding model behind every edge platform
- AWS CloudFront Cache Behaviors for Media — the equivalent Accept negotiation when you run on CloudFront instead
- Cache-Control Headers for Image and Video Assets — immutable URLs and TTL discipline the derived variants depend on