Cloudflare Polish vs Image Resizing: tradeoffs

Both features shrink images at Cloudflare’s edge, so teams reach for one, see a byte saving, and never evaluate the other — usually leaving the larger win on the table. This guide — part of Cloudflare Image Resizing and Polish within CDN & Edge Media Delivery — frames the choice honestly. Polish is free, automatic, and cannot resize; Image Resizing is billed, explicit, and derives any dimension you want. The right answer is often “both, on different images,” and this page shows how to draw that line by cost, plan, and image role.

Before you can compare them fairly

The one distinction that drives everything

Polish re-encodes the bytes an image already has; Image Resizing produces new bytes at a new size. That single difference cascades into every other tradeoff:

  • Polish cannot make a 3000-pixel master smaller in dimensions, only in codec. The device still decodes 3000 pixels.
  • Image Resizing produces the exact pixels the slot needs, which is where the order-of-magnitude payload savings live — but it charges per unique transformation and needs either a paid plan or a Worker.

If your images are already served near their display size (a CMS that exports at render width, avatars uploaded at 96 px), Polish captures nearly the whole opportunity for free. If your images are oversized masters dropped into responsive layouts, Polish leaves most of the waste in place and Resizing is the real fix.

Choosing between Polish and Image Resizing A decision flow: first ask whether the served image is already close to its display size. If yes, Polish alone captures the win. If no, ask whether you can change markup or add a Worker. If yes, use Image Resizing for correct dimensions. If markup is fixed but bytes still matter, use Polish as the free fallback. A final node notes combining both across different images. Image already near its display size? Yes Polish only free, zero config No Can edit markup or add a Worker? Yes Image Resizing correct pixels, billed No Polish as free fallback codec win without dimension fix combine: Resizing on heroes, Polish on the rest

Where each engine sits in the request path

The two features are not alternatives at the same layer — they attach to the request at completely different points, and that is why they never collide.

Polish is a response-path filter. The URL stays exactly what your HTML already asks for. On a cache miss the edge fetches the master from origin, and Polish re-encodes the bytes on the way into the cache. Three settings govern it: lossless strips metadata and repacks the existing codec without touching pixel values; lossy additionally re-encodes JPEG at a reduced internal quality target; and the WebP/AVIF conversion option layers a next-gen encode on top of either mode, gated on the request Accept header. Because the negotiated variants live under the same URL, the object is stored with Vary: Accept — the mechanism described in Cache-Control headers for image and video assets.

Polish also declines work, silently, and tells you so. If the WebP encode comes out larger than the source, or the source is already an optimally packed PNG, or the file is a format Polish does not handle, the original bytes pass through and the cf-polished header records the reason:

cf-polished value Meaning
origFmt=jpeg, origSize=283946 Polish processed the object; the header reports the source format and byte count
status=webp_bigger The WebP encode was larger than the original; the original was served
status=webp_same The WebP encode saved nothing measurable; the original was served
status=already_optimized The source was already at or below Polish’s re-encode target
status=not_needed The object was too small or of a type Polish skips
(header absent) Polish never ran — feature off, or the cached object predates enabling it

Image Resizing is a request-path interceptor. The /cdn-cgi/image/ prefix is recognised before normal origin routing happens, so the edge never asks your origin for that path. It parses the comma-separated option string, issues its own internal subrequest for the master named in the tail of the URL, transforms the decoded pixels, and stores the derived variant under a key that includes the full option string. The cf-resized header appears only on responses that this path produced.

Polish and Image Resizing request pipelines compared Two horizontal pipelines of five stages each. The upper Polish pipeline: the browser requests the ordinary image URL, the edge cache misses, the origin returns the two-thousand-pixel master, Polish re-encodes it at the same dimensions, and the response carries a cf-polished header. The lower Image Resizing pipeline: the browser requests a slash cdn-cgi slash image URL, the edge intercepts it and parses the option string before origin routing, an internal subrequest fetches the master once, the edge resizes and encodes a 640-pixel AVIF, and the response carries a cf-resized header. Polish — response-path re-encode, URL unchanged Browser GET /media/ hero.jpg Edge cache MISS key = same URL Origin 2000×1333 JPEG master Polish lossy re-encode + WebP / AVIF Response still 2000×1333 cf-polished set Pixels unchanged — the device still decodes 2000×1333. Payload falls roughly 40%. Image Resizing — request-path interception, URL rewritten Browser GET /cdn-cgi/ image/width=640… Edge intercept parse options before routing Subrequest master fetched once, then reused Transform resize 640×427 encode AVIF q=75 Response 640×427 variant cf-resized set Pixels correct — decode cost falls with the payload. Payload falls roughly 98%. Boundary rule: a /cdn-cgi/image/ URL is served by Image Resizing only; every other image URL still flows through Polish.

Decision matrix

Dimension Polish Image Resizing
Changes pixel dimensions No Yes (width, height, dpr, fit)
Auto WebP/AVIF from Accept Yes (WebP/AVIF toggle) Yes (format=auto)
Configuration effort One dashboard toggle Per-image URL or a Worker
Plan requirement Pro and up (dashboard toggle) Paid tier for URL form, or any plan via Workers
Billing model Included, no per-image charge Billed per unique transformation
Works on legacy <img src> unchanged Yes No — the URL must become /cdn-cgi/image/…
Fixes oversized-image decode cost No Yes
Per-request crop / focal point No Yes (gravity, fit=cover)
Cache key Original URL plus Vary: Accept URL including the full option string
Behaviour when it cannot process Passes the original through, reason in cf-polished onerror decides: error, or redirect to the source
EXIF / metadata handling Stripped as part of the re-encode Controlled explicitly by metadata=
Animation control Animation preserved as-is anim=false collapses to the first frame
Point of attachment Response path, after origin fetch Request path, before origin routing
Diagnostic header cf-polished cf-resized
Best for Bulk of already-sized images Heroes, galleries, responsive srcset

Tradeoff: Polish’s zero-effort appeal is real, but its ceiling is low on sites whose images are oversized. On a page where a 2 MB master paints into a 500-pixel column, Polish might return 1.2 MB (WebP) while Resizing returns 40 KB. The 30× difference is not a codec difference — it is a dimensions difference Polish structurally cannot touch.

What the gap actually looks like in bytes

Put the three outcomes on one scale and the argument stops being philosophical. The measurements below are for a single 2000×1333 photographic master painting into a 640-pixel column:

Stage Delivered bytes What actually changed Pixels decoded
Origin master, untouched 2,048 KB nothing 2.67 M
Polish, lossy + WebP 1,200 KB codec and quality only 2.67 M
Image Resizing, width=640,format=auto 40 KB dimensions and codec 0.27 M
Delivered bytes: master versus Polish versus Image Resizing A horizontal bar chart on a linear kilobyte scale. The origin master bar reaches 2,048 kilobytes. The Polish lossy plus WebP bar reaches 1,200 kilobytes, a little over half. The Image Resizing bar at width 640 with format auto is 40 kilobytes, a sliver barely visible against the axis. Annotations note that the thirty-fold gap between the second and third bars comes from dimensions rather than codec. One 2000×1333 master delivered into a 640 px column 2,048 KB Origin master 2000×1333 JPEG 1,200 KB Polish lossy + WebP 40 KB Image Resizing width=640, AVIF 0 512 KB 1,024 KB 1,536 KB 2,048 KB The 30× gap between rows 2 and 3 is dimensions, not codec — Polish cannot cross it. Row 3 also decodes 90% fewer pixels, which is the half of the win LCP actually feels.

Two details in that table matter more than the headline ratio. First, the Polish row is not a bad result in isolation — a 41% reduction for one dashboard toggle is excellent value per unit of effort, and it is what the codec comparison in AVIF vs WebP compression benchmarks predicts for a photographic JPEG. Second, the decoded-pixel column is the one that shows up in main-thread time. A 2.67-megapixel decode costs the same CPU whether the bytes arrived as JPEG or WebP; only the resize removes it. On low-end Android that decode is frequently the difference between an LCP inside and outside the 2.5-second threshold.

Cost and plan considerations

The two features price completely differently, and conflating them causes budget surprises:

  • Polish carries no per-image cost. Once enabled on an eligible plan it re-encodes everything the zone serves, forever, at no marginal charge. Its “cost” is entirely the missed opportunity of not resizing.
  • Image Resizing bills per unique transformation — a distinct combination of source plus option string, counted the first time it is generated. Deliveries from cache are free; it is the generation of a new variant that meters. This makes variant discipline financial, not just architectural.
// Cost intuition: transformations = unique (source × option-string) pairs.
// This snippet estimates monthly transforms from your breakpoint plan so you
// can price Image Resizing before enabling it.
const sources = 12000;        // distinct master images across the site
const breakpoints = 4;        // widths you actually emit in srcset (e.g. 320/640/960/1280)
const formats = 1;            // format=auto counts once per width, negotiated internally
// Each master generates (breakpoints × formats) variants the first time each
// is requested. Continuous/interpolated widths would multiply this by 100s —
// which is exactly why clamping widths to a fixed set is a COST control.
const monthlyTransforms = sources * breakpoints * formats;
console.log(`~${monthlyTransforms.toLocaleString()} transformations to warm the cache`);
// ~48,000 — compare against your plan's included transform quota.

Warning: the fastest way to blow the transformation budget is to let client JavaScript pass continuous width values (window.innerWidth) straight into the URL. Every distinct pixel value is a new billed transform and a new cache entry. Snap widths to a fixed breakpoint set — the clamp pattern in Configuring Cloudflare Image Resizing URL parameters is a cost control as much as a cache one.

Three second-order costs are easy to miss when you model this. The Worker route is metered twice: the transformation is billed, and so is the Worker invocation that requested it, so a Worker that runs on every image request has a floor cost independent of how many transforms it triggers. Purges are expensive in the same currency — invalidating a master evicts every derived variant, and the next request for each one re-bills the transform, which makes aggressive cache-busting a recurring line item rather than a one-off. And a low hit rate quietly re-bills: variants evicted under cache pressure at a given colo are regenerated the next time that colo is asked, so a rarely-requested tail of rarely-requested widths can meter repeatedly across the year even though the option strings never changed. Polish has none of these characteristics, which is a genuine argument for leaving it to handle the rarely-requested tail.

When to combine both

Combining is the common production answer, drawn along image role:

  • Image Resizing on the images where dimensions dominate the payload: the LCP hero, product galleries, anything wired into a responsive srcset and sizes set. These justify the per-transform cost because the savings are largest and the traffic is highest.
  • Polish left enabled underneath for the bulk of remaining images: user-uploaded avatars served at their natural size, blog inline images from a CMS that already exports at render width, legacy templates you have not migrated to /cdn-cgi/image/. Polish captures the codec win on all of them for free, with no markup changes.

The two do not collide. Once a URL is a /cdn-cgi/image/ transformation, Image Resizing owns its format via format=auto and Polish does not re-touch it; everything not wrapped in a resizing URL still flows through Polish. You get resized-and-negotiated heroes and codec-optimized everything-else from one configuration.

Tradeoff: running both means two diagnostic headers to reason about (cf-resized versus cf-polished) and two mental models for TTL — Resizing variants take their TTL from the resizing request’s cache settings, Polish images inherit the origin’s Cache-Control. Keep master URLs content-hashed so both models stay safe under long TTLs, per Cache-Control headers for image and video assets.

Verifying which engine handled a response

Neither feature announces itself in the rendered page, so every claim above has to be checked at the header level before you trust a rollout.

# 1. Confirm Polish ran on an ordinary, untransformed URL.
#    --compressed is deliberately omitted: it affects transfer encoding, not
#    the image codec, and would confuse the byte comparison in step 3.
curl -sI -H 'Accept: image/avif,image/webp,*/*;q=0.8' \
  'https://example.com/media/blog/diagram.png' \
  | grep -iE 'content-type|cf-polished|cf-cache-status'
# cf-polished: origFmt=png, origSize=184203   -> Polish processed the object
# cf-polished: status=webp_bigger             -> Polish ran and declined; original served
# (no cf-polished at all)                     -> Polish off, or the cached copy predates it

# 2. Confirm Image Resizing — and NOT Polish — owns the transformed URL.
#    cf-resized present with cf-polished absent is the documented boundary.
curl -sI -H 'Accept: image/avif,image/webp,*/*;q=0.8' \
  'https://example.com/cdn-cgi/image/width=640,format=auto/media/blog/diagram.png' \
  | grep -iE 'content-type|cf-resized|cf-polished|cf-cache-status'

# 3. Prove the dimension win rather than the codec win, by measuring both
#    responses. %{size_download} reports the body bytes actually received.
for u in \
  'https://example.com/media/blog/diagram.png' \
  'https://example.com/cdn-cgi/image/width=640,format=auto/media/blog/diagram.png'; do
  curl -s -o /dev/null -H 'Accept: image/avif,image/webp,*/*;q=0.8' \
    -w '%{size_download} bytes  %{content_type}  <- '"$u"'\n' "$u"
done

Warning: run step 1 twice. The first response after enabling Polish is frequently the un-polished object still sitting in the edge cache, and reading that as “Polish is broken” is the single most common false alarm. Purge the path, request it again, then read cf-polished.

Common mistakes

1. Enabling Polish and calling image optimization “done”

Symptom: payloads dropped ~40% but LCP barely moved on image-heavy pages. Cause: Polish fixed the codec but not the oversized dimensions driving decode and transfer. Fix: move hero and gallery images to Image Resizing with real width breakpoints; keep Polish for the rest.

2. Reaching for Image Resizing on an already-right-sized site

Symptom: a transformation bill for images that were already served at display size. Cause: paying per transform to shrink pixels that did not need shrinking. Fix: if masters already match their slots, Polish alone captures the codec win for free — skip Resizing there.

3. Expecting Polish to crop or focal-point

Symptom: need a square avatar from a rectangular upload; Polish does nothing. Cause: Polish never changes geometry. Fix: cropping requires fit=cover + gravity on Image Resizing.

4. Assuming the two features fight over format

Symptom: worry that Polish will “double-encode” a resized image. Cause: misunderstanding the boundary. Fix: they are mutually exclusive per response — /cdn-cgi/image/ URLs are handled by Resizing only; confirm with the cf-resized versus cf-polished header.

5. Unbounded variant growth on Image Resizing

Symptom: cache hit rate low, transform count climbing every month. Cause: continuous or per-user widths creating endless unique option strings. Fix: enumerate a small breakpoint set and clamp all width inputs to it.

FAQ

Do I need to purge the cache after enabling Polish?

Yes, if you want the change to show up immediately. Polish runs on the response path when an object is pulled from origin, so anything already stored at the edge keeps being served in its original form until it expires. Purge the image prefixes to force a re-fetch, then re-check cf-polished.

Can Polish output AVIF, and should I turn that on?

Polish emits AVIF when the WebP/AVIF conversion option is enabled and the client’s Accept advertises AVIF support. It is worth enabling, because the failure mode is benign: when the AVIF encode is not smaller than the source, Polish serves the original and records status=webp_bigger rather than shipping a regression. The cost is a slightly longer first-byte time on the cold path while the encode happens.

Can I use Image Resizing without a paid plan?

The /cdn-cgi/image/ URL form requires a paid plan, but cf.image on a Worker fetch() runs on any plan that has Workers. That route is also how you keep resizing URLs out of your HTML — the Worker rewrites requests server-side, which is the only practical option when the markup is generated by a CMS you do not control.