↑ top
← Back to blog
EN | ES

Edge Caching on Cloudflare Workers Without a Paid CDN

Your Worker is already the CDN. Here's how to use Cache-Control and s-maxage to serve images and static assets at near-zero cost.

✦ AI Summary

Cloudflare Worker is not an origin server that sits behind a CDN. Every request hits one of Cloudflare's 300+ edge locations. The response you return can be cached right there for the next visitor in the same region.

Edge Caching on Cloudflare Workers Without a Paid CDN

When you deploy a Cloudflare Worker, you're not deploying an origin server that sits behind a CDN. The Worker is the CDN. Every request hits one of Cloudflare's 300+ edge locations, and the response you return can be cached right there for the next visitor in the same region.

Most tutorials miss this. They treat Workers like a Node.js server and bolt on a caching layer later. You don't need to.

How Worker Caching Works

When your Worker returns a response with the right Cache-Control headers, Cloudflare's edge caches that response automatically. No extra configuration, no paid plan, no Cache Rules to set up.

javascriptreturn new Response(obj.body, {
  headers: {
    "Content-Type": "image/webp",
    "Cache-Control": "public, s-maxage=31536000, max-age=86400",
  },
});

Two directives do the heavy lifting:

  • s-maxage=31536000 — the edge (Cloudflare) caches this for 1 year
  • max-age=86400 — the browser caches it for 1 day

After the first visitor in a region triggers the Worker and fetches the image from R2, every subsequent visitor in that region gets the cached response. The Worker doesn't even run. R2 doesn't get hit. Cost: essentially zero.

What to Cache and For How Long

The right TTL depends on how often the content changes.

Images and static assets — content-addressed by nature (a new post gets a new slug), so cache aggressively. One year for s-maxage is not excessive.

javascript"Cache-Control": "public, s-maxage=31536000, max-age=86400"

RSS and sitemap — change when you publish, which is infrequent. One hour for the edge is fine. Short browser TTL so feed readers get fresh content.

javascript"Cache-Control": "public, s-maxage=3600, max-age=300"

API endpoints with dynamic data — reactions, view counts, anything that changes on user interaction. Don't cache.

javascript"Cache-Control": "no-store"

HTML pages — tricky. I don't cache them at the edge because the Worker is fast enough (< 50ms cold start on Cloudflare) and the content changes when I publish new posts. If you have a high-traffic site, you could cache HTML with a short TTL and purge on deploy.

Serving R2 Objects

R2 objects store their original Content-Type as metadata. The writeHttpMetadata() method copies it to your response headers automatically — you don't have to hardcode it.

javascriptconst obj = await env.POSTS_BUCKET.get(slug);
const headers = new Headers();
obj.writeHttpMetadata(headers);
headers.set("Cache-Control", "public, s-maxage=31536000, max-age=86400");
return new Response(obj.body, { headers });

This matters for the OG image route — the content type is image/webp, and writeHttpMetadata puts it in the response without you having to know what format the file is in.

The Hidden Benefit: No Origin Egress

On most hosting setups, CDN → Origin bandwidth costs money. On Cloudflare Workers + R2, Worker requests are free on the paid plan and R2 egress to Cloudflare's network is free. Serving 10,000 images per day costs the same as serving 100.

The architecture removes the cost structure that normally forces you to think carefully about caching. You still should think about it — cache hits are faster — but you won't get a surprise invoice if you forget.

One Gotcha: Cache Varies by Region

Cloudflare caches per PoP (point of presence). A visitor in São Paulo and a visitor in New York will each trigger a cache miss on their first request, then get cached responses for subsequent visits. There's no global single-origin cache.

This means cold start latency (Worker running + R2 fetch) is distributed across first visitors per region, not just the very first visitor globally. For a personal site with low traffic, this is invisible. For a viral post, the first visitor in each major city takes the hit.

It's not a problem. It's just the model.

Was this helpful?