Image resizer caching

Hi all!

I want to resize images from an external origin. Images of that source has a cache-control header with value: ‘public, max-age=3600’
But I want to cache the resized image on the CF edge for longer than that time. Probably more than half a year, or longer. So that image doesn’t count on billable usage requests.

I use these options on the cf object: = 31536000 (one year) = true

The CF wiki says: “Results of image processing are cached for one hour or longer if origin server’s Cache-Control header allows.”
But from time to time I see still revalidations. What did I wrong?

Is the resize image shared over the CF network, or is every resized image on a each CF datacenter, a requests that counts on billable requests?

Below my worker code.

addEventListener("fetch", event => {
 * Fetch and log a request
 * @param {Request} request
async function handleRequest(request) {
  // Parse request URL to get access to query string
  let url = new URL(request.url)

  // Cloudflare-specific options are in the cf object
  let options = { cf: { image: {} } }

  // Copy parameters from query string to request options
  if (url.searchParams.has("w")) = url.searchParams.get("w")
  if (url.searchParams.has("h")) = url.searchParams.get("h")

  // set sharpen param =  0.2

  // use webp if supported
  const accept = request.headers.get('Accept')
  if (accept && accept.includes('image/webp')) { = 'webp'

  // TODO decide later if we like to support avif
  // use avif or webm if supported, avif is prefered
  //if (accept.includes('image/avif')) {
  // = 'avif'
  //} else if (accept.includes('image/webp')) {
  // = 'webm'

  // Always cache this fetch for a max of x seconds before revalidating the resource for $$$ reasons = EDGE_CACHE_TTL, = true

  // Get URL of the original (full size) image to resize
  const imageURL = url.searchParams.get("s")
  if (!imageURL) return new Response('Missing "s" value', { status: 400 })

  try {
    const { hostname, pathname } = new URL(imageURL)

    // Not every url has the extension already in it,
    // so this is disabled for now.
    // // Only accept JPEG, PNG, GIF, or WebP types
    // // @see
    // if (!/\.(jpe?g|png|gif|webp)$/i.test(pathname)) {
    //   return new Response('Invalid image type', { status: 400 })
    // }

    // Only accept domains in ALLOWED_DOMAINS setting
    const domains = ALLOWED_DOMAINS.split(/\r?\n/g).map(item=>item.trim().replace('*', '')).join('|')
    const domainsRegex = new RegExp(domains, 'g')
    if (domainsRegex && !domainsRegex.test(hostname)) {
      return new Response('Domain of src image not in list', { status: 403 })

  } catch (err) {
    return new Response('Invalid "image" value ' + err, { status: 400 })

  // Build a request that passes through request header, so that automatic format negotiation can work
  const imageRequest = new Request(imageURL, {
    headers: request.headers

  // Returning fetch() with resizing options will pass through response with the resized image
  let response = await fetch(imageRequest, options)
  response = new Response(response.body, response)

  // Set cache control headers to cache on browser for x minutes
  response.headers.set("Cache-Control", "public, max-age=" + BROWSER_CACHE_TTL)

  return response